Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 23 Apr 2015 17:22:50 -0400
changeset 240864 2a1ddf08a4b4ee8914e99139efcaa64b11318c0b
parent 240863 2f2888ca42c2dc0939d92ab302f4f37f425c3f19 (current diff)
parent 240781 22a157f7feb7bec54cff9bfcf61f6e96ec2903d0 (diff)
child 240865 2250a0d01517f6256d949e2afbf24118d18d4f43
push id28647
push usercbook@mozilla.com
push dateFri, 24 Apr 2015 12:37:38 +0000
treeherdermozilla-central@86d3308ec888 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone40.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 m-c to inbound. a=merge CLOSED TREE
browser/app/profile/firefox.js
browser/base/content/browser.js
browser/components/sessionstore/test/browser_history_cap.js
toolkit/devtools/gcli/source/lib/gcli/api.js
toolkit/devtools/gcli/source/lib/gcli/connectors/index.js
toolkit/devtools/gcli/source/lib/gcli/connectors/protocol.js
toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js
toolkit/devtools/gcli/source/lib/gcli/connectors/remoted.js
toolkit/devtools/gcli/source/lib/gcli/mozui/ffdisplay.js
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0c5e2ee1173f3c53379ef3cd10de714836258fe8"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="90f848a40efad820ab00fa52bec52dff37255b12"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="0c5e2ee1173f3c53379ef3cd10de714836258fe8"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9a9797062c6001d6346504161c51187a2968466b"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="46e1877c0d88b085f7ebc5f432d5bb8f1e2d1f3b"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0c5e2ee1173f3c53379ef3cd10de714836258fe8"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="90f848a40efad820ab00fa52bec52dff37255b12"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0c5e2ee1173f3c53379ef3cd10de714836258fe8"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="90f848a40efad820ab00fa52bec52dff37255b12"/>
--- a/b2g/config/emulator-l/sources.xml
+++ b/b2g/config/emulator-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0c5e2ee1173f3c53379ef3cd10de714836258fe8"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="90f848a40efad820ab00fa52bec52dff37255b12"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="173b3104bfcbd23fc9dccd4b0035fc49aae3d444">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="0c5e2ee1173f3c53379ef3cd10de714836258fe8"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="9a9797062c6001d6346504161c51187a2968466b"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="46e1877c0d88b085f7ebc5f432d5bb8f1e2d1f3b"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="ef937d1aca7c4cf89ecb5cc43ae8c21c2000a9db">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0c5e2ee1173f3c53379ef3cd10de714836258fe8"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="90f848a40efad820ab00fa52bec52dff37255b12"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0c5e2ee1173f3c53379ef3cd10de714836258fe8"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="90f848a40efad820ab00fa52bec52dff37255b12"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
-        "git_revision": "4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd", 
+        "git_revision": "0c5e2ee1173f3c53379ef3cd10de714836258fe8", 
         "remote": "https://git.mozilla.org/releases/gaia.git", 
         "branch": ""
     }, 
-    "revision": "3057b08e087df6d1ee21ca3a3741e56bb78021a1", 
+    "revision": "910522407a1dd40c9302f2e342f8cf1d4828fc3b", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="4efd19d199ae52656604f794c5a77518400220fd">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0c5e2ee1173f3c53379ef3cd10de714836258fe8"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="90f848a40efad820ab00fa52bec52dff37255b12"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/nexus-5-l/sources.xml
+++ b/b2g/config/nexus-5-l/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="61e82f99bb8bc78d52b5717e9a2481ec7267fa33">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="4d87112bbf48cdd09c19e553cc9aebd2a2c4ddfd"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="0c5e2ee1173f3c53379ef3cd10de714836258fe8"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="d3868ff4bb3a4b81382795e2784258c210fe6cb8"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="adb24954bf8068f21705b570450475d183336b2d"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="90f848a40efad820ab00fa52bec52dff37255b12"/>
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1593,16 +1593,21 @@ pref("devtools.editor.autocomplete", tru
 // Enable the Font Inspector
 pref("devtools.fontinspector.enabled", true);
 
 // Pref to store the browser version at the time of a telemetry ping for an
 // opened developer tool. This allows us to ping telemetry just once per browser
 // version for each user.
 pref("devtools.telemetry.tools.opened.version", "{}");
 
+// Set imgur upload client ID
+pref("devtools.gcli.imgurClientID", '0df414e888d7240');
+// Imgur's upload URL
+pref("devtools.gcli.imgurUploadURL", "https://api.imgur.com/3/image");
+
 // Whether the character encoding menu is under the main Firefox button. This
 // preference is a string so that localizers can alter it.
 pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties");
 
 // Allow using tab-modal prompts when possible.
 pref("prompts.tab_modal.enabled", true);
 // Whether the Panorama should animate going in/out of tabs
 pref("browser.panorama.animate_zoom", true);
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -51,49 +51,55 @@ var StarUI = {
     this._blockedCommands.forEach(function (elt) {
       if (elt.getAttribute("wasDisabled") != "true")
         elt.removeAttribute("disabled");
       elt.removeAttribute("wasDisabled");
     });
   },
 
   // nsIDOMEventListener
-  handleEvent: function SU_handleEvent(aEvent) {
+  handleEvent(aEvent) {
     switch (aEvent.type) {
       case "popuphidden":
         if (aEvent.originalTarget == this.panel) {
           if (!this._element("editBookmarkPanelContent").hidden)
             this.quitEditMode();
 
           if (this._anchorToolbarButton) {
             this._anchorToolbarButton.removeAttribute("open");
             this._anchorToolbarButton = null;
           }
           this._restoreCommandsState();
           this._itemId = -1;
-          if (this._batching) {
-            PlacesUtils.transactionManager.endBatch(false);
-            this._batching = false;
-          }
+          if (this._batching)
+            this.endBatch();
 
           switch (this._actionOnHide) {
             case "cancel": {
-              PlacesUtils.transactionManager.undoTransaction();
+              if (!PlacesUIUtils.useAsyncTransactions) {
+                PlacesUtils.transactionManager.undoTransaction();
+                break;
+              }
+              PlacesTransactions.undo().catch(Cu.reportError);
               break;
             }
             case "remove": {
               // Remove all bookmarks for the bookmark's url, this also removes
               // the tags for the url.
-              PlacesUtils.transactionManager.beginBatch(null);
-              let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval);
-              for (let i = 0; i < itemIds.length; i++) {
-                let txn = new PlacesRemoveItemTransaction(itemIds[i]);
-                PlacesUtils.transactionManager.doTransaction(txn);
+              if (!PlacesUIUtils.useAsyncTransactions) {
+                let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval);
+                for (let itemId of itemIds) {
+                  let txn = new PlacesRemoveItemTransaction(itemId);
+                  PlacesUtils.transactionManager.doTransaction(txn);
+                }
+                break;
               }
-              PlacesUtils.transactionManager.endBatch(false);
+
+              PlacesTransactions.RemoveBookmarksForUrls(this._uriForRemoval)
+                                .transact().catch(Cu.reportError);
               break;
             }
           }
           this._actionOnHide = "";
         }
         break;
       case "keypress":
         if (aEvent.defaultPrevented) {
@@ -117,48 +123,62 @@ var StarUI = {
             break;
         }
         break;
     }
   },
 
   _overlayLoaded: false,
   _overlayLoading: false,
-  showEditBookmarkPopup:
-  function SU_showEditBookmarkPopup(aItemId, aAnchorElement, aPosition) {
+  showEditBookmarkPopup: Task.async(function* (aNode, aAnchorElement, aPosition) {
+    // TODO: Deprecate this once async transactions are enabled and the legacy
+    // transactions code is gone (bug 1131491) - we don't want addons to to use
+    // the  completeNodeLikeObjectForItemId, so it's better if they keep passing
+    // the item-id for now).
+    if (typeof(aNode) == "number") {
+      let itemId = aNode;
+      if (PlacesUIUtils.useAsyncTransactions) {
+        let guid = yield PlacesUtils.promiseItemGuid(itemId);
+        aNode = yield PlacesUIUtils.promiseNodeLike(guid);
+      }
+      else {
+        aNode = { itemId };
+        yield PlacesUIUtils.completeNodeLikeObjectForItemId(aNode);
+      }
+    }
+
     // Performance: load the overlay the first time the panel is opened
     // (see bug 392443).
     if (this._overlayLoading)
       return;
 
     if (this._overlayLoaded) {
-      this._doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition);
+      this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
       return;
     }
 
     this._overlayLoading = true;
     document.loadOverlay(
       "chrome://browser/content/places/editBookmarkOverlay.xul",
       (function (aSubject, aTopic, aData) {
         // Move the header (star, title, button) into the grid,
         // so that it aligns nicely with the other items (bug 484022).
         let header = this._element("editBookmarkPanelHeader");
         let rows = this._element("editBookmarkPanelGrid").lastChild;
         rows.insertBefore(header, rows.firstChild);
         header.hidden = false;
 
         this._overlayLoading = false;
         this._overlayLoaded = true;
-        this._doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition);
+        this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition);
       }).bind(this)
     );
-  },
+  }),
 
-  _doShowEditBookmarkPanel:
-  function SU__doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition) {
+  _doShowEditBookmarkPanel: Task.async(function* (aNode, aAnchorElement, aPosition) {
     if (this.panel.state != "closed")
       return;
 
     this._blockCommands(); // un-done in the popuphiding handler
 
     // Set panel title:
     // if we are batching, i.e. the bookmark has been added now,
     // then show Page Bookmarked, else if the bookmark did already exist,
@@ -174,25 +194,25 @@ var StarUI = {
     this._element("editBookmarkPanelContent").hidden = false;
 
     // The remove button is shown only if we're not already batching, i.e.
     // if the cancel button/ESC does not remove the bookmark.
     this._element("editBookmarkPanelRemoveButton").hidden = this._batching;
 
     // The label of the remove button differs if the URI is bookmarked
     // multiple times.
-    var bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI);
-    var forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
-    var label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length);
+    let bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI);
+    let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
+    let label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length);
     this._element("editBookmarkPanelRemoveButton").label = label;
 
     // unset the unstarred state, if set
     this._element("editBookmarkPanelStarIcon").removeAttribute("unstarred");
 
-    this._itemId = aItemId !== undefined ? aItemId : this._itemId;
+    this._itemId = aNode.itemId;
     this.beginBatch();
 
     if (aAnchorElement) {
       // Set the open=true attribute if the anchor is a
       // descendent of a toolbarbutton.
       let parent = aAnchorElement.parentNode;
       while (parent) {
         if (parent.localName == "toolbarbutton") {
@@ -202,20 +222,20 @@ var StarUI = {
       }
       if (parent) {
         this._anchorToolbarButton = parent;
         parent.setAttribute("open", "true");
       }
     }
     this.panel.openPopup(aAnchorElement, aPosition);
 
-    gEditItemOverlay.initPanel(this._itemId,
-                               { hiddenRows: ["description", "location",
+    gEditItemOverlay.initPanel({ node: aNode
+                               , hiddenRows: ["description", "location",
                                               "loadInSidebar", "keyword"] });
-  },
+  }),
 
   panelShown:
   function SU_panelShown(aEvent) {
     if (aEvent.target == this.panel) {
       if (!this._element("editBookmarkPanelContent").hidden) {
         let fieldToFocus = "editBMPanel_" +
           gPrefService.getCharPref("browser.bookmarks.editDialog.firstEditField");
         var elt = this._element(fieldToFocus);
@@ -242,40 +262,76 @@ var StarUI = {
   },
 
   removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() {
     this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
     this._actionOnHide = "remove";
     this.panel.hidePopup();
   },
 
-  beginBatch: function SU_beginBatch() {
-    if (!this._batching) {
+  // Matching the way it is used in the Library, editBookmarkOverlay implements
+  // an instant-apply UI, having no batched-Undo/Redo support.
+  // However, in this context (the Star UI) we have a Cancel button whose
+  // expected behavior is to undo all the operations done in the panel.
+  // Sometime in the future this needs to be reimplemented using a
+  // non-instant apply code path, but for the time being, we patch-around
+  // editBookmarkOverlay so that all of the actions done in the panel
+  // are treated by PlacesTransactions as a single batch.  To do so,
+  // we start a PlacesTransactions batch when the star UI panel is shown, and
+  // we keep the batch ongoing until the panel is hidden.
+  _batchBlockingDeferred: null,
+  beginBatch() {
+    if (this._batching)
+      return;
+    if (PlacesUIUtils.useAsyncTransactions) {
+      this._batchBlockingDeferred = PromiseUtils.defer();
+      PlacesTransactions.batch(function* () {
+        yield this._batchBlockingDeferred.promise;
+      }.bind(this));
+    }
+    else {
       PlacesUtils.transactionManager.beginBatch(null);
-      this._batching = true;
     }
+    this._batching = true;
+  },
+
+  endBatch() {
+    if (!this._batching)
+      return;
+
+    if (PlacesUIUtils.useAsyncTransactions) {
+      this._batchBlockingDeferred.resolve();
+      this._batchBlockingDeferred = null;
+    }
+    else {
+      PlacesUtils.transactionManager.endBatch(false);
+    }
+    this._batching = false;
   }
-}
+};
 
 ////////////////////////////////////////////////////////////////////////////////
 //// PlacesCommandHook
 
 var PlacesCommandHook = {
   /**
    * Adds a bookmark to the page loaded in the given browser.
    *
    * @param aBrowser
    *        a <browser> element.
    * @param [optional] aParent
    *        The folder in which to create a new bookmark if the page loaded in
    *        aBrowser isn't bookmarked yet, defaults to the unfiled root.
    * @param [optional] aShowEditUI
    *        whether or not to show the edit-bookmark UI for the bookmark item
-   */  
-  bookmarkPage: function PCH_bookmarkPage(aBrowser, aParent, aShowEditUI) {
+   */
+  bookmarkPage: Task.async(function* (aBrowser, aParent, aShowEditUI) {
+    if (PlacesUIUtils.useAsyncTransactions)
+      return (yield this._bookmarkPagePT(aBrowser, aParent, aShowEditUI));
+
     var uri = aBrowser.currentURI;
     var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
     if (itemId == -1) {
       // Bug 1148838 - Make this code work for full page plugins.
       var title;
       var description;
       var charset;
       try {
@@ -291,25 +347,25 @@ var PlacesCommandHook = {
 
       if (aShowEditUI) {
         // If we bookmark the page here (i.e. page was not "starred" already)
         // but open right into the "edit" state, start batching here, so
         // "Cancel" in that state removes the bookmark.
         StarUI.beginBatch();
       }
 
-      var parent = aParent != undefined ?
+      var parent = aParent !== undefined ?
                    aParent : PlacesUtils.unfiledBookmarksFolderId;
       var descAnno = { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
       var txn = new PlacesCreateBookmarkTransaction(uri, parent, 
                                                     PlacesUtils.bookmarks.DEFAULT_INDEX,
                                                     title, null, [descAnno]);
       PlacesUtils.transactionManager.doTransaction(txn);
       itemId = txn.item.id;
-      // Set the character-set
+      // Set the character-set.
       if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
         PlacesUtils.setCharsetForURI(uri, charset);
     }
 
     // Revert the contents of the location bar
     if (gURLBar)
       gURLBar.handleRevert();
 
@@ -329,17 +385,92 @@ var PlacesCommandHook = {
 
     let pageProxyFavicon = document.getElementById("page-proxy-favicon");
     if (isElementVisible(pageProxyFavicon)) {
       StarUI.showEditBookmarkPopup(itemId, pageProxyFavicon,
                                    "bottomcenter topright");
     } else {
       StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap");
     }
-  },
+  }),
+
+  // TODO: Replace bookmarkPage code with this function once legacy
+  // transactions are removed.
+  _bookmarkPagePT: Task.async(function* (aBrowser, aParentId, aShowEditUI) {
+    let url = new URL(aBrowser.currentURI.spec);
+    let info = yield PlacesUtils.bookmarks.fetch({ url });
+    if (!info) {
+      let parentGuid = aParentId !== undefined ?
+                         yield PlacesUtils.promiseItemGuid(aParentId) :
+                         PlacesUtils.bookmarks.unfiledGuid;
+      info = { url, parentGuid };
+      // Bug 1148838 - Make this code work for full page plugins.
+      let description = null;
+      let charset = null;
+      try {
+        let isErrorPage = /^about:(neterror|certerror|blocked)/
+                          .test(aBrowser.contentDocumentAsCPOW.documentURI);
+        info.title = isErrorPage ?
+          (yield PlacesUtils.promisePlaceInfo(aBrowser.currentURI)).title :
+          aBrowser.contentTitle;
+        info.title = info.title || url.href;
+        description = PlacesUIUtils.getDescriptionFromDocument(aBrowser.contentDocumentAsCPOW);
+        charset = aBrowser.characterSet;
+      }
+      catch (e) {
+      	Components.utils.reportError(e);
+      }
+
+      if (aShowEditUI) {
+        // If we bookmark the page here (i.e. page was not "starred" already)
+        // but open right into the "edit" state, start batching here, so
+        // "Cancel" in that state removes the bookmark.
+        StarUI.beginBatch();
+      }
+
+      if (description) {
+        info.annotations = [{ name: PlacesUIUtils.DESCRIPTION_ANNO
+                            , value: description }];
+      }
+
+      info.guid = yield PlacesTransactions.NewBookmark(info).transact();
+
+      // Set the character-set
+      if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
+      	 PlacesUtils.setCharsetForURI(makeURI(url.href), charset);
+    }
+
+    // Revert the contents of the location bar
+    if (gURLBar)
+      gURLBar.handleRevert();
+
+    // If it was not requested to open directly in "edit" mode, we are done.
+    if (!aShowEditUI)
+      return;
+
+    let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(info);
+
+    // Try to dock the panel to:
+    // 1. the bookmarks menu button
+    // 2. the page-proxy-favicon
+    // 3. the content area
+    if (BookmarkingUI.anchor) {
+      StarUI.showEditBookmarkPopup(node, BookmarkingUI.anchor,
+                                   "bottomcenter topright");
+      return;
+    }
+
+    let pageProxyFavicon = document.getElementById("page-proxy-favicon");
+    if (isElementVisible(pageProxyFavicon)) {
+      StarUI.showEditBookmarkPopup(node, pageProxyFavicon,
+                                   "bottomcenter topright");
+    } else {
+      StarUI.showEditBookmarkPopup(node, aBrowser, "overlap");
+    }
+  }),
 
   /**
    * Adds a bookmark to the page loaded in the current tab. 
    */
   bookmarkCurrentPage: function PCH_bookmarkCurrentPage(aShowEditUI, aParent) {
     this.bookmarkPage(gBrowser.selectedBrowser, aParent, aShowEditUI);
   },
 
@@ -348,37 +479,48 @@ var PlacesCommandHook = {
    * @param aParent
    *        The folder in which to create a new bookmark if aURL isn't
    *        bookmarked.
    * @param aURL (string)
    *        the address of the link target
    * @param aTitle
    *        The link text
    */
-  bookmarkLink: function PCH_bookmarkLink(aParent, aURL, aTitle) {
-    var linkURI = makeURI(aURL);
-    var itemId = PlacesUtils.getMostRecentBookmarkForURI(linkURI);
-    if (itemId == -1) {
+  bookmarkLink: Task.async(function* (aParentId, aURL, aTitle) {
+    let node = null;
+    if (PlacesUIUtils.useAsyncTransactions) {
+      node = yield PlacesUIUtils.fetchNodeLike({ url: aURL });
+    }
+    else {
+      let linkURI = makeURI(aURL);
+      let itemId = PlacesUtils.getMostRecentBookmarkForURI(linkURI);
+      if (itemId != -1) {
+        node = { itemId, uri: aURL };
+        PlacesUIUtils.completeNodeLikeObjectForItemId(node);
+      }
+    }
+
+    if (node) {
+      PlacesUIUtils.showBookmarkDialog({ action: "edit"
+                                       , type: "bookmark"
+                                       , node
+                                       }, window);
+    }
+    else {
       PlacesUIUtils.showBookmarkDialog({ action: "add"
                                        , type: "bookmark"
                                        , uri: linkURI
                                        , title: aTitle
                                        , hiddenRows: [ "description"
                                                      , "location"
                                                      , "loadInSidebar"
                                                      , "keyword" ]
                                        }, window);
     }
-    else {
-      PlacesUIUtils.showBookmarkDialog({ action: "edit"
-                                       , type: "bookmark"
-                                       , itemId: itemId
-                                       }, window);
-    }
-  },
+  }),
 
   /**
    * List of nsIURI objects characterizing the tabs currently open in the
    * browser, modulo pinned tabs.  The URIs will be in the order in which their
    * corresponding tabs appeared and duplicates are discarded.
    */
   get uniqueCurrentPages() {
     let uniquePages = {};
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -19,16 +19,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
                                   "resource:///modules/BrowserUITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                   "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+                                  "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
                                   "resource://gre/modules/CharsetMenu.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
                                   "resource://gre/modules/ShortcutUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "GMPInstallManager",
                                   "resource://gre/modules/GMPInstallManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
                                   "resource://gre/modules/NewTabUtils.jsm");
@@ -44,16 +46,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "@mozilla.org/browser/favicon-service;1",
                                    "mozIAsyncFavicons");
 XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
                                    "@mozilla.org/network/dns-service;1",
                                    "nsIDNSService");
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 
+
 const nsIWebNavigation = Ci.nsIWebNavigation;
 
 var gLastBrowserCharset = null;
 var gProxyFavIcon = null;
 var gLastValidURLStr = "";
 var gInPrintPreviewMode = false;
 var gContextMenu = null; // nsContextMenu instance
 var gMultiProcessBrowser =
@@ -2379,16 +2382,18 @@ function URLBarSetURI(aURI) {
 
   gURLBar.value = value;
   gURLBar.valueIsTyped = !valid;
   SetPageProxyState(valid ? "valid" : "invalid");
 }
 
 function losslessDecodeURI(aURI) {
   var value = aURI.spec;
+  if (aURI.schemeIs("moz-action"))
+    throw new Error("losslessDecodeURI should never get a moz-action URI");
   // Try to decode as UTF-8 if there's no encoding sequence that we would break.
   if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value))
     try {
       value = decodeURI(value)
                 // 1. decodeURI decodes %25 to %, which creates unintended
                 //    encoding sequences. Re-encode it, unless it's part of
                 //    a sequence that survived decodeURI, i.e. one for:
                 //    ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -461,16 +461,17 @@ skip-if = (os == "win" && !debug) || e10
 [browser_windowopen_reflows.js]
 skip-if = buildapp == 'mulet'
 [browser_wyciwyg_urlbarCopying.js]
 [browser_zbug569342.js]
 skip-if = e10s # Bug 1094240 - has findbar-related failures
 [browser_registerProtocolHandler_notification.js]
 skip-if = e10s # Bug 940206 - nsIWebContentHandlerRegistrar::registerProtocolHandler doesn't work in e10s
 [browser_no_mcb_on_http_site.js]
+[browser_bug1104165-switchtab-decodeuri.js]
 [browser_bug1003461-switchtab-override.js]
 [browser_bug1024133-switchtab-override-keynav.js]
 [browser_bug1025195_switchToTabHavingURI_aOpenParams.js]
 [browser_addCertException.js]
 skip-if = e10s # Bug 1100687 - test directly manipulates content (content.document.getElementById)
 [browser_bug1045809.js]
 [browser_e10s_switchbrowser.js]
 [browser_e10s_about_process.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug1104165-switchtab-decodeuri.js
@@ -0,0 +1,29 @@
+add_task(function* test_switchtab_decodeuri() {
+  info("Opening first tab");
+  let tab = gBrowser.addTab("http://example.org/browser/browser/base/content/test/general/dummy_page.html#test%7C1");
+  yield promiseTabLoadEvent(tab);
+
+  info("Opening and selecting second tab");
+  let newTab = gBrowser.selectedTab = gBrowser.addTab();
+
+  info("Wait for autocomplete")
+  yield promiseAutocompleteResultPopup("dummy_page");
+
+  info("Select autocomplete popup entry");
+  EventUtils.synthesizeKey("VK_DOWN" , {});
+  ok(gURLBar.value.startsWith("moz-action:switchtab"), "switch to tab entry found");
+
+  info("switch-to-tab");
+  yield new Promise((resolve, reject) => {
+    // In case of success it should switch tab.
+    gBrowser.tabContainer.addEventListener("TabSelect", function select() {
+      gBrowser.tabContainer.removeEventListener("TabSelect", select, false);
+      is(gBrowser.selectedTab, tab, "Should have switched to the right tab");
+      resolve();
+    }, false);
+    EventUtils.synthesizeKey("VK_RETURN" , { });
+  });
+
+  gBrowser.removeCurrentTab();
+  yield PlacesTestUtils.clearHistory();
+});
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -300,17 +300,17 @@ file, You can obtain one at http://mozil
 
           let matchLastLocationChange = true;
           if (action) {
             if (action.type == "switchtab") {
               url = action.params.url;
               if (this.hasAttribute("actiontype")) {
                 this.handleRevert();
                 let prevTab = gBrowser.selectedTab;
-                if (switchToTabHavingURI(url) &&
+                if (switchToTabHavingURI(action.params.originalUrl || url) &&
                     isTabEmpty(prevTab))
                   gBrowser.removeTab(prevTab);
                 return;
               }
             } else if (action.type == "keyword") {
               url = action.params.url;
             } else if (action.type == "searchengine") {
               let engine = Services.search.getEngineByName(action.params.engineName);
@@ -711,19 +711,34 @@ file, You can obtain one at http://mozil
       </method>
 
       <property name="textValue">
         <getter><![CDATA[
           return this.inputField.value;
         ]]></getter>
         <setter>
           <![CDATA[
+          let uri;
           try {
-            val = losslessDecodeURI(makeURI(val));
-          } catch (ex) { }
+            uri = makeURI(val);
+          } catch (ex) {}
+
+          if (uri) {
+            let action = this._parseActionUrl(val);
+            if (action) {
+              if (action.params.url) {
+                // Store the original URL in the action URL.
+                action.params.originalUrl = action.params.url;
+                action.params.url = losslessDecodeURI(makeURI(action.params.url));
+                val = "moz-action:" + action.type + "," + JSON.stringify(action.params);
+              }
+            } else {
+              val = losslessDecodeURI(uri);
+            }
+          }
 
           // Trim popup selected values, but never trim results coming from
           // autofill.
           if (this.popup.selectedIndex == -1 ||
               this.mController.getStyleAt(this.popup.selectedIndex) == "autofill") {
             this._disableTrim = true;
           }
           this.value = val;
@@ -753,16 +768,22 @@ file, You can obtain one at http://mozil
           let [, type, params] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
 
           let action = {
             type: type,
           };
 
           try {
             action.params = JSON.parse(params);
+            if (action.params.input) {
+              action.params.input = decodeURIComponent(action.params.input);
+            }
+            if (action.params.searchQuery) {
+              action.params.searchQuery = decodeURIComponent(action.params.searchQuery);
+            }
           } catch (e) {
             // If this failed, we assume that params is not a JSON object, and
             // is instead just a flat string. This will happen when
             // UnifiedComplete is disabled - in which case, the param is always
             // a URL.
             action.params = {
               url: params,
             }
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -1206,16 +1206,165 @@ this.PlacesUIUtils = {
 
   shouldEnableTabsFromOtherComputersMenuitem: function() {
     let weaveEnabled = Weave.Service.isLoggedIn &&
                        Weave.Service.engineManager.get("tabs") &&
                        Weave.Service.engineManager.get("tabs").enabled;
     let cloudSyncEnabled = CloudSync && CloudSync.ready && CloudSync().tabsReady && CloudSync().tabs.hasRemoteTabs();
     return weaveEnabled || cloudSyncEnabled;
   },
+
+  /**
+   * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT'S LIKELY TO BE REMOVED IN A
+   * FUTURE RELEASE.
+   *
+   * Checks if a place: href represents a folder shortcut.
+   *
+   * @param queryString
+   *        the query string to check (a place: href)
+   * @return whether or not queryString represents a folder shortcut.
+   * @throws if queryString is malformed.
+   */
+  isFolderShortcutQueryString(queryString) {
+    // Based on GetSimpleBookmarksQueryFolder in nsNavHistory.cpp.
+
+    let queriesParam = { }, optionsParam = { };
+    PlacesUtils.history.queryStringToQueries(queryString,
+                                             queriesParam,
+                                             { },
+                                             optionsParam);
+    let queries = queries.value;
+    if (queries.length == 0)
+      throw new Error(`Invalid place: uri: ${queryString}`);
+    return queries.length == 1 &&
+           queries[0].folderCount == 1 &&
+           !queries[0].hasBeginTime &&
+           !queries[0].hasEndTime &&
+           !queries[0].hasDomain &&
+           !queries[0].hasURI &&
+           !queries[0].hasSearchTerms &&
+           !queries[0].tags.length == 0 &&
+           optionsParam.value.maxResults == 0;
+  },
+
+  /**
+   * WARNING TO ADDON AUTHORS: DO NOT USE THIS METHOD. IT"S LIKELY TO BE REMOVED IN A
+   * FUTURE RELEASE.
+   *
+   * Helpers for consumers of editBookmarkOverlay which don't have a node as their input.
+   * Given a partial node-like object, having at least the itemId property set, this
+   * method completes the rest of the properties necessary for initialising the edit
+   * overlay with it.
+   *
+   * @param aNodeLike
+   *        an object having at least the itemId nsINavHistoryResultNode property set,
+   *        along with any other properties available.
+   */
+  completeNodeLikeObjectForItemId(aNodeLike) {
+    if (this.useAsyncTransactions) {
+      // When async-transactions are enabled, node-likes must have
+      // bookmarkGuid set, and we cannot set it synchronously.
+      throw new Error("completeNodeLikeObjectForItemId cannot be used when " +
+                      "async transactions are enabled");
+    }
+    if (!("itemId" in aNodeLike))
+      throw new Error("itemId missing in aNodeLike");
+
+    let itemId = aNodeLike.itemId;
+    let defGetter = XPCOMUtils.defineLazyGetter.bind(XPCOMUtils, aNodeLike);
+
+    if (!("title" in aNodeLike))
+      defGetter("title", () => PlacesUtils.bookmarks.getItemTitle(itemId));
+
+    if (!("uri" in aNodeLike)) {
+      defGetter("uri", () => {
+        let uri = null;
+        try {
+          uri = PlacesUtils.bookmarks.getBookmarkURI(itemId);
+        }
+        catch(ex) { }
+        return uri ? uri.spec : "";
+      });
+    }
+
+    if (!("type" in aNodeLike)) {
+      defGetter("type", () => {
+        if (aNodeLike.uri.length > 0) {
+          if (/^place:/.test(aNodeLike.uri)) {
+            if (this.isFolderShortcutQueryString(aNodeLike.uri))
+              return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+
+            return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
+          }
+
+          return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+        }
+
+        let itemType = PlacesUtils.bookmarks.getItemType(itemId);
+        if (itemType == PlacesUtils.bookmarks.TYPE_FOLDER)
+          return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
+
+        throw new Error("Unexpected item type");
+      });
+    }
+  },
+
+  /**
+   * Helpers for consumers of editBookmarkOverlay which don't have a node as their input.
+   *
+   * Given a bookmark object for either a url bookmark or a folder, returned by
+   * Bookmarks.fetch (see Bookmark.jsm), this creates a node-like object suitable for
+   * initialising the edit overlay with it.
+   *
+   * @param aFetchInfo
+   *        a bookmark object returned by Bookmarks.fetch.
+   * @return a node-like object suitable for initialising editBookmarkOverlay.
+   * @throws if aFetchInfo is representing a separator.
+   */
+  promiseNodeLikeFromFetchInfo: Task.async(function* (aFetchInfo) {
+    if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR)
+      throw new Error("promiseNodeLike doesn't support separators");
+
+    return Object.freeze({
+      itemId: yield PlacesUtils.promiseItemId(aFetchInfo.guid),
+      bookmarkGuid: aFetchInfo.guid,
+      title: aFetchInfo.title,
+      uri: aFetchInfo.url !== undefined ? aFetchInfo.url.href : "",
+
+      get type() {
+        if (aFetchInfo.itemType == PlacesUtils.bookmarks.TYPE_FOLDER)
+          return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER;
+
+        if (this.uri.length == 0)
+          throw new Error("Unexpected item type");
+
+        if (/^place:/.test(this.uri)) {
+          if (this.isFolderShortcutQueryString(this.uri))
+            return Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+
+          return Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
+        }
+
+        return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+      }
+    });
+  }),
+
+  /**
+   * Shortcut for calling promiseNodeLikeFromFetchInfo on the result of
+   * Bookmarks.fetch for the given guid/info object.
+   *
+   * @see promiseNodeLikeFromFetchInfo above and Bookmarks.fetch in Bookmarks.jsm.
+   */
+  fetchNodeLike: Task.async(function* (aGuidOrInfo) {
+    let info = yield PlacesUtils.bookmarks.fetch(aGuidOrInfo);
+    if (!info)
+      return null;
+    return (yield this.promiseNodeLikeFromFetchInfo(info));
+  })
 };
 
 
 PlacesUIUtils.PLACES_FLAVORS = [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
                                 PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
                                 PlacesUtils.TYPE_X_MOZ_PLACE];
 
 PlacesUIUtils.URI_FLAVORS = [PlacesUtils.TYPE_X_MOZ_URL,
--- a/browser/components/places/content/bookmarkProperties.js
+++ b/browser/components/places/content/bookmarkProperties.js
@@ -32,40 +32,43 @@
  *           with the given uri. If the dialog is set to adding a folder with
  *           bookmark items under it (see URIList), a default static title is
  *           used ("[Folder Name]").
  *        2) The index field of the default insertion point is ignored if
  *           the folder picker is shown.
  *     - "edit" - for editing a bookmark item or a folder.
  *       @ type (String). Possible values:
  *         - "bookmark"
- *           @ itemId (Integer) - the id of the bookmark item.
+ *           @ node (an nsINavHistoryResultNode object) - a node representing
+ *             the bookmark.
  *         - "folder" (also applies to livemarks)
- *           @ itemId (Integer) - the id of the folder.
+ *           @ node (an nsINavHistoryResultNode object) - a node representing
+ *             the folder.
  *   @ hiddenRows (Strings array) - optional, list of rows to be hidden
  *     regardless of the item edited or added by the dialog.
  *     Possible values:
  *     - "title"
  *     - "location"
  *     - "description"
  *     - "keyword"
  *     - "tags"
  *     - "loadInSidebar"
- *     - "feedLocation"
- *     - "siteLocation"
  *     - "folderPicker" - hides both the tree and the menu.
- *   @ readOnly (Boolean) - optional, states if the panel should be read-only
  *
  * window.arguments[0].performed is set to true if any transaction has
  * been performed by the dialog.
  */
 
 Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+                                  "resource://gre/modules/PromiseUtils.jsm");
 
 const BOOKMARK_ITEM = 0;
 const BOOKMARK_FOLDER = 1;
 const LIVEMARK_CONTAINER = 2;
 
 const ACTION_EDIT = 0;
 const ACTION_ADD = 1;
 
@@ -92,17 +95,16 @@ var BookmarkPropertiesPanel = {
   _postData: null,
   _charSet: "",
   _feedURI: null,
   _siteURI: null,
 
   _defaultInsertionPoint: null,
   _hiddenRows: [],
   _batching: false,
-  _readOnly: false,
 
   /**
    * This method returns the correct label for the dialog's "accept"
    * button based on the variant of the dialog.
    */
   _getAcceptLabel: function BPP__getAcceptLabel() {
     if (this._action == ACTION_ADD) {
       if (this._URIs.length)
@@ -141,34 +143,35 @@ var BookmarkPropertiesPanel = {
       return this._strings.getFormattedString("dialogTitleEdit", [this._title]);
     }
     return "";
   },
 
   /**
    * Determines the initial data for the item edited or added by this dialog
    */
-  _determineItemInfo: function BPP__determineItemInfo() {
-    var dialogInfo = window.arguments[0];
+  _determineItemInfo() {
+    let dialogInfo = window.arguments[0];
     this._action = dialogInfo.action == "add" ? ACTION_ADD : ACTION_EDIT;
     this._hiddenRows = dialogInfo.hiddenRows ? dialogInfo.hiddenRows : [];
     if (this._action == ACTION_ADD) {
       NS_ASSERT("type" in dialogInfo, "missing type property for add action");
 
       if ("title" in dialogInfo)
         this._title = dialogInfo.title;
 
       if ("defaultInsertionPoint" in dialogInfo) {
         this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint;
       }
-      else
+      else {
         this._defaultInsertionPoint =
           new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
                              PlacesUtils.bookmarks.DEFAULT_INDEX,
                              Ci.nsITreeView.DROP_ON);
+      }
 
       switch (dialogInfo.type) {
         case "bookmark":
           this._itemType = BOOKMARK_ITEM;
           if ("uri" in dialogInfo) {
             NS_ASSERT(dialogInfo.uri instanceof Ci.nsIURI,
                       "uri property should be a uri object");
             this._uri = dialogInfo.uri;
@@ -225,62 +228,25 @@ var BookmarkPropertiesPanel = {
               this._title = this._strings.getString("newLivemarkDefault");
           }
       }
 
       if ("description" in dialogInfo)
         this._description = dialogInfo.description;
     }
     else { // edit
-      NS_ASSERT("itemId" in dialogInfo);
-      this._itemId = dialogInfo.itemId;
-      this._title = PlacesUtils.bookmarks.getItemTitle(this._itemId);
-      this._readOnly = !!dialogInfo.readOnly;
-
+      this._node = dialogInfo.node;
       switch (dialogInfo.type) {
         case "bookmark":
           this._itemType = BOOKMARK_ITEM;
-
-          this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
-          // keyword
-          this._keyword = PlacesUtils.bookmarks
-                                     .getKeywordForBookmark(this._itemId);
-          // Load In Sidebar
-          this._loadInSidebar = PlacesUtils.annotations
-                                           .itemHasAnnotation(this._itemId,
-                                                              PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
           break;
-
         case "folder":
           this._itemType = BOOKMARK_FOLDER;
-          PlacesUtils.livemarks.getLivemark({ id: this._itemId })
-            .then(aLivemark => {
-              this._itemType = LIVEMARK_CONTAINER;
-              this._feedURI = aLivemark.feedURI;
-              this._siteURI = aLivemark.siteURI;
-              this._fillEditProperties();
-
-              let acceptButton = document.documentElement.getButton("accept");
-              acceptButton.disabled = !this._inputIsValid();
-
-              let newHeight = window.outerHeight +
-                              this._element("descriptionField").boxObject.height;
-              window.resizeTo(window.outerWidth, newHeight);
-            }, () => undefined);
-
           break;
       }
-
-      // Description
-      if (PlacesUtils.annotations
-                     .itemHasAnnotation(this._itemId, PlacesUIUtils.DESCRIPTION_ANNO)) {
-        this._description = PlacesUtils.annotations
-                                       .getItemAnnotation(this._itemId,
-                                                          PlacesUIUtils.DESCRIPTION_ANNO);
-      }
     }
   },
 
   /**
    * This method returns the title string corresponding to a given URI.
    * If none is available from the bookmark service (probably because
    * the given URI doesn't appear in bookmarks or history), we synthesize
    * a title from the first 100 characters of the URI.
@@ -296,83 +262,92 @@ var BookmarkPropertiesPanel = {
     // get the title from History
     return PlacesUtils.history.getPageTitle(aURI);
   },
 
   /**
    * This method should be called by the onload of the Bookmark Properties
    * dialog to initialize the state of the panel.
    */
-  onDialogLoad: function BPP_onDialogLoad() {
+  onDialogLoad: Task.async(function* () {
     this._determineItemInfo();
 
     document.title = this._getDialogTitle();
     var acceptButton = document.documentElement.getButton("accept");
     acceptButton.label = this._getAcceptLabel();
 
     this._beginBatch();
 
     switch (this._action) {
       case ACTION_EDIT:
-        this._fillEditProperties();
-        acceptButton.disabled = this._readOnly;
+        gEditItemOverlay.initPanel({ node: this._node
+                                   , hiddenRows: this._hiddenRows });
+        acceptButton.disabled = gEditItemOverlay.readOnly;
         break;
       case ACTION_ADD:
-        this._fillAddProperties();
+        this._node = yield this._promiseNewItem();
+        // Edit the new item
+        gEditItemOverlay.initPanel({ node: this._node
+                                   , hiddenRows: this._hiddenRows });
+
+        // Empty location field if the uri is about:blank, this way inserting a new
+        // url will be easier for the user, Accept button will be automatically
+        // disabled by the input listener until the user fills the field.
+        let locationField = this._element("locationField");
+        if (locationField.value == "about:blank")
+          locationField.value = "";
+
         // if this is an uri related dialog disable accept button until
         // the user fills an uri value.
         if (this._itemType == BOOKMARK_ITEM)
           acceptButton.disabled = !this._inputIsValid();
         break;
     }
 
+    // Adjust the dialog size to the changes done by initPanel. This is necessary because
+    // initPanel, which shows and hides elements, may run after some async work was done
+    // here - i.e. after the DOM load event was processed.
+    window.sizeToContent();
+
     // When collapsible elements change their collapsed attribute we must
     // resize the dialog.
     // sizeToContent is not usable due to bug 90276, so we'll use resizeTo
     // instead and cache the element size. See WSucks in the legacy
     // UI code (addBookmark2.js).
     if (!this._element("tagsRow").collapsed) {
       this._element("tagsSelectorRow")
           .addEventListener("DOMAttrModified", this, false);
     }
     if (!this._element("folderRow").collapsed) {
       this._element("folderTreeRow")
           .addEventListener("DOMAttrModified", this, false);
     }
 
-    if (!this._readOnly) {
+    if (!gEditItemOverlay.readOnly) {
       // Listen on uri fields to enable accept button if input is valid
       if (this._itemType == BOOKMARK_ITEM) {
         this._element("locationField")
             .addEventListener("input", this, false);
         if (this._isAddKeywordDialog) {
           this._element("keywordField")
               .addEventListener("input", this, false);
         }
       }
-      else if (this._itemType == LIVEMARK_CONTAINER) {
-        this._element("feedLocationField")
-            .addEventListener("input", this, false);
-        this._element("siteLocationField")
-            .addEventListener("input", this, false);
-      }
     }
 
     window.sizeToContent();
-  },
+  }),
 
   // nsIDOMEventListener
   _elementsHeight: [],
   handleEvent: function BPP_handleEvent(aEvent) {
     var target = aEvent.target;
     switch (aEvent.type) {
       case "input":
         if (target.id == "editBMPanel_locationField" ||
-            target.id == "editBMPanel_feedLocationField" ||
-            target.id == "editBMPanel_siteLocationField" ||
             target.id == "editBMPanel_keywordField") {
           // Check uri fields to enable accept button if input is valid
           document.documentElement
                   .getButton("accept").disabled = !this._inputIsValid();
         }
         break;
 
       case "DOMAttrModified":
@@ -392,97 +367,94 @@ var BookmarkPropertiesPanel = {
           }
 
           window.resizeTo(window.outerWidth, newHeight);
         }
         break;
     }
   },
 
-  _beginBatch: function BPP__beginBatch() {
+	// Hack for implementing batched-Undo around the editBookmarkOverlay
+	// instant-apply code. For all the details see the comment above beginBatch
+	// in browser-places.js
+  _batchBlockingDeferred: null,
+  _beginBatch() {
     if (this._batching)
       return;
-
-    PlacesUtils.transactionManager.beginBatch(null);
+    if (PlacesUIUtils.useAsyncTransactions) {
+      this._batchBlockingDeferred = PromiseUtils.defer();
+      PlacesTransactions.batch(function* () {
+        yield this._batchBlockingDeferred.promise;
+      }.bind(this));
+    }
+    else {
+      PlacesUtils.transactionManager.beginBatch(null);
+    }
     this._batching = true;
   },
 
-  _endBatch: function BPP__endBatch() {
+  _endBatch() {
     if (!this._batching)
       return;
 
-    PlacesUtils.transactionManager.endBatch(false);
+    if (PlacesUIUtils.useAsyncTransactions) {
+      this._batchBlockingDeferred.resolve();
+      this._batchBlockingDeferred = null;
+    }
+    else {
+      PlacesUtils.transactionManager.endBatch(false);
+    }
     this._batching = false;
   },
 
-  _fillEditProperties: function BPP__fillEditProperties() {
-    gEditItemOverlay.initPanel(this._itemId,
-                               { hiddenRows: this._hiddenRows,
-                                 forceReadOnly: this._readOnly });
-  },
-
-  _fillAddProperties: function BPP__fillAddProperties() {
-    this._createNewItem();
-    // Edit the new item
-    gEditItemOverlay.initPanel(this._itemId,
-                               { hiddenRows: this._hiddenRows });
-    // Empty location field if the uri is about:blank, this way inserting a new
-    // url will be easier for the user, Accept button will be automatically
-    // disabled by the input listener until the user fills the field.
-    var locationField = this._element("locationField");
-    if (locationField.value == "about:blank")
-      locationField.value = "";
-  },
-
   // nsISupports
   QueryInterface: function BPP_QueryInterface(aIID) {
     if (aIID.equals(Ci.nsIDOMEventListener) ||
         aIID.equals(Ci.nsISupports))
       return this;
 
     throw Cr.NS_NOINTERFACE;
   },
 
   _element: function BPP__element(aID) {
     return document.getElementById("editBMPanel_" + aID);
   },
 
-  onDialogUnload: function BPP_onDialogUnload() {
+  onDialogUnload() {
     // gEditItemOverlay does not exist anymore here, so don't rely on it.
     // Calling removeEventListener with arguments which do not identify any
     // currently registered EventListener on the EventTarget has no effect.
     this._element("tagsSelectorRow")
         .removeEventListener("DOMAttrModified", this, false);
     this._element("folderTreeRow")
         .removeEventListener("DOMAttrModified", this, false);
     this._element("locationField")
         .removeEventListener("input", this, false);
-    this._element("feedLocationField")
-        .removeEventListener("input", this, false);
-    this._element("siteLocationField")
-        .removeEventListener("input", this, false);
   },
 
-  onDialogAccept: function BPP_onDialogAccept() {
+  onDialogAccept() {
     // We must blur current focused element to save its changes correctly
     document.commandDispatcher.focusedElement.blur();
     // The order here is important! We have to uninit the panel first, otherwise
     // late changes could force it to commit more transactions.
     gEditItemOverlay.uninitPanel(true);
     this._endBatch();
     window.arguments[0].performed = true;
   },
 
-  onDialogCancel: function BPP_onDialogCancel() {
+  onDialogCancel() {
     // The order here is important! We have to uninit the panel first, otherwise
     // changes done as part of Undo may change the panel contents and by
     // that force it to commit more transactions.
     gEditItemOverlay.uninitPanel(true);
     this._endBatch();
-    PlacesUtils.transactionManager.undoTransaction();
+    if (PlacesUIUtils.useAsyncTransactions)
+      PlacesTransactions.undo().catch(Components.utils.reportError);
+    else
+      PlacesUtils.transactionManager.undoTransaction();
     window.arguments[0].performed = false;
   },
 
   /**
    * This method checks to see if the input fields are in a valid state.
    *
    * @returns  true if the input is valid, false otherwise
    */
@@ -578,25 +550,24 @@ var BookmarkPropertiesPanel = {
   },
 
   /**
    * Returns a childItems-transactions array representing the URIList with
    * which the dialog has been opened.
    */
   _getTransactionsForURIList: function BPP__getTransactionsForURIList() {
     var transactions = [];
-    for (var i = 0; i < this._URIs.length; ++i) {
-      var uri = this._URIs[i];
-      var title = this._getURITitleFromHistory(uri);
-      var createTxn = new PlacesCreateBookmarkTransaction(uri, -1, 
+    for (let uri of this._URIs) {
+      let title = this._getURITitleFromHistory(uri);
+      let createTxn = new PlacesCreateBookmarkTransaction(uri, -1,
                                                           PlacesUtils.bookmarks.DEFAULT_INDEX,
                                                           title);
       transactions.push(createTxn);
     }
-    return transactions; 
+    return transactions;
   },
 
   /**
    * Returns a transaction for creating a new folder item representing the
    * various fields and opening arguments of the dialog.
    */
   _getCreateNewFolderTransaction:
   function BPP__getCreateNewFolderTransaction(aContainer, aIndex) {
@@ -619,30 +590,108 @@ var BookmarkPropertiesPanel = {
    */
   _getCreateNewLivemarkTransaction:
   function BPP__getCreateNewLivemarkTransaction(aContainer, aIndex) {
     return new PlacesCreateLivemarkTransaction(this._feedURI, this._siteURI,
                                                this._title,
                                                aContainer, aIndex);
   },
 
-  /**
-   * Dialog-accept code-path for creating a new item (any type)
-   */
   _createNewItem: function BPP__getCreateItemTransaction() {
     var [container, index] = this._getInsertionPointDetails();
     var txn;
 
     switch (this._itemType) {
       case BOOKMARK_FOLDER:
         txn = this._getCreateNewFolderTransaction(container, index);
         break;
       case LIVEMARK_CONTAINER:
         txn = this._getCreateNewLivemarkTransaction(container, index);
-        break;      
+        break;
       default: // BOOKMARK_ITEM
         txn = this._getCreateNewBookmarkTransaction(container, index);
     }
 
     PlacesUtils.transactionManager.doTransaction(txn);
     this._itemId = PlacesUtils.bookmarks.getIdForItemAt(container, index);
-  }
+
+    return Object.freeze({
+      itemId: this._itemId,
+      get bookmarkGuid() {
+        throw new Error("Node-like bookmarkGuid getter called even though " +
+                        "async transactions are disabled");
+      },
+      title: this._title,
+      uri: this._uri ? this._uri.spec : "",
+      type: this._itemType == BOOKMARK_ITEM ?
+              Ci.nsINavHistoryResultNode.RESULT_TYPE_URI :
+              Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER
+    });
+  },
+
+  _promiseNewItem: Task.async(function* () {
+    if (!PlacesUIUtils.useAsyncTransactions)
+      return this._createNewItem();
+
+    let txnFunc =
+      { [BOOKMARK_FOLDER]: PlacesTransactions.NewFolder,
+        [LIVEMARK_CONTAINER]: PlacesTransactions.NewLivemark,
+        [BOOKMARK_ITEM]: PlacesTransactions.NewBookmark
+      }[this._itemType];
+
+    let [containerId, index] = this._getInsertionPointDetails();
+    let parentGuid = yield PlacesUtils.promiseItemGuid(containerId);
+    let annotations = [];
+    if (this._description) {
+      annotations.push({ name: PlacesUIUtils.DESCRIPTION_ANNO
+                       , value: this._description });
+    }
+    if (this._loadInSidebar) {
+      annotations.push({ name: PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO
+                       , value: true });
+    }
+
+    let itemGuid;
+    let info = { parentGuid, index, title: this._title, annotations };
+    if (this._itemType == BOOKMARK_ITEM) {
+      info.url = this._uri;
+      if (this._keyword)
+        info.keyword = this._keyword;
+      if (this._postData)
+        info.postData = this._postData;
+
+      if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window))
+        PlacesUtils.setCharsetForURI(this._uri, this._charSet);
+
+      itemGuid = yield PlacesTransactions.NewBookmark(info).transact();
+    }
+    else if (this._itemType == LIVEMARK_CONTAINER) {
+      info.feedUrl = this._feedURI;
+      if (this._siteURI)
+        info.siteUrl = this._siteURI;
+
+      itemGuid = yield PlacesTransactions.NewLivemark(info).transact();
+    }
+    else if (this._itemType == BOOKMARK_FOLDER) {
+      itemGuid = yield PlacesTransactions.NewFolder(info).transact();
+      for (let uri of this._URIs) {
+        let placeInfo = yield PlacesUtils.promisePlaceInfo(uri);
+        let title = placeInfo ? placeInfo.title : "";
+        yield PlacesTransactions.transact({ parentGuid: itemGuid, uri, title });
+      }
+    }
+    else {
+      throw new Error(`unexpected value for _itemType:  ${this._itemType}`);
+    }
+
+    this._itemGuid = itemGuid;
+    this._itemId = yield PlacesUtils.promiseItemId(itemGuid);
+    return Object.freeze({
+      itemId: this._itemId,
+      bookmarkGuid: this._itemGuid,
+      title: this._title,
+      uri: this._uri ? this._uri.spec : "",
+      type: this._itemType == BOOKMARK_ITEM ?
+              Ci.nsINavHistoryResultNode.RESULT_TYPE_URI :
+              Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER
+    });
+  })
 };
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -124,25 +124,16 @@ PlacesController.prototype = {
 
     // All other Places Commands are prefixed with "placesCmd_" ... this
     // filters out other commands that we do _not_ support (see 329587).
     const CMD_PREFIX = "placesCmd_";
     return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
   },
 
   isCommandEnabled: function PC_isCommandEnabled(aCommand) {
-    if (PlacesUIUtils.useAsyncTransactions) {
-      switch (aCommand) {
-      case "placesCmd_new:folder":
-      case "placesCmd_new:bookmark":
-      case "placesCmd_createBookmark":
-        return false;
-      }
-    }
-
     switch (aCommand) {
     case "cmd_undo":
       if (!PlacesUIUtils.useAsyncTransactions)
         return PlacesUtils.transactionManager.numberOfUndoItems > 0;
 
       return PlacesTransactions.topUndoEntry != null;
     case "cmd_redo":
       if (!PlacesUIUtils.useAsyncTransactions)
@@ -274,17 +265,17 @@ PlacesController.prototype = {
       break;
     case "placesCmd_new:folder":
       this.newItem("folder");
       break;
     case "placesCmd_new:bookmark":
       this.newItem("bookmark");
       break;
     case "placesCmd_new:separator":
-      this.newSeparator().then(null, Components.utils.reportError);
+      this.newSeparator().catch(Cu.reportError);
       break;
     case "placesCmd_show:info":
       this.showBookmarkPropertiesForSelection();
       break;
     case "placesCmd_moveBookmarks":
       this.moveSelectedBookmarks();
       break;
     case "placesCmd_reload":
@@ -661,37 +652,23 @@ PlacesController.prototype = {
    */
   selectAll: function PC_selectAll() {
     this._view.selectAll();
   },
 
   /**
    * Opens the bookmark properties for the selected URI Node.
    */
-  showBookmarkPropertiesForSelection:
-  function PC_showBookmarkPropertiesForSelection() {
-    var node = this._view.selectedNode;
+  showBookmarkPropertiesForSelection() {
+    let node = this._view.selectedNode;
     if (!node)
       return;
 
-    var itemType = PlacesUtils.nodeIsFolder(node) ||
-                   PlacesUtils.nodeIsTagQuery(node) ? "folder" : "bookmark";
-    var concreteId = PlacesUtils.getConcreteItemId(node);
-    var isRootItem = PlacesUtils.isRootItem(concreteId);
-    var itemId = node.itemId;
-    if (isRootItem || PlacesUtils.nodeIsTagQuery(node)) {
-      // If this is a root or the Tags query we use the concrete itemId to catch
-      // the correct title for the node.
-      itemId = concreteId;
-    }
-
     PlacesUIUtils.showBookmarkDialog({ action: "edit"
-                                     , type: itemType
-                                     , itemId: itemId
-                                     , readOnly: isRootItem
+                                     , node
                                      , hiddenRows: [ "folderPicker" ]
                                      }, window.top);
   },
 
   /**
    * This method can be run on a URI parameter to ensure that it didn't
    * receive a string instead of an nsIURI object.
    */
--- a/browser/components/places/content/editBookmarkOverlay.js
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -1,271 +1,290 @@
 /* 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/. */
 
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+
 const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
 const MAX_FOLDER_ITEM_IN_MENU_LIST = 5;
 
-var gEditItemOverlay = {
-  _uri: null,
-  _itemId: -1,
-  _itemIds: [],
-  _uris: [],
-  _tags: [],
-  _allTags: [],
-  _multiEdit: false,
-  _itemType: -1,
-  _readOnly: false,
-  _hiddenRows: [],
+let gEditItemOverlay = {
   _observersAdded: false,
   _staticFoldersListBuilt: false,
-  _initialized: false,
-  _titleOverride: "",
+
+  _paneInfo: null,
+  _setPaneInfo(aInitInfo) {
+    if (!aInitInfo)
+      return this._paneInfo = null;
+
+    if ("uris" in aInitInfo && "node" in aInitInfo)
+      throw new Error("ambiguous pane info");
+    if (!("uris" in aInitInfo) && !("node" in aInitInfo))
+      throw new Error("Neither node nor uris set for pane info");
+
+    let node = "node" in aInitInfo ? aInitInfo.node : null;
+
+    // Since there's no true UI for folder shortcuts (they show up just as their target
+    // folders), when the pane shows for them it's opened in read-only mode, showing the
+    // properties of the target folder.
+    let itemId = node ? node.itemId : -1;
+    let itemGuid = PlacesUIUtils.useAsyncTransactions && node ?
+                     PlacesUtils.getConcreteItemGuid(node) : null;
+    let isItem = itemId != -1;
+    let isFolderShortcut = isItem &&
+      node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
+    let isURI = node && PlacesUtils.nodeIsURI(node);
+    let uri = isURI ? NetUtil.newURI(node.uri) : null;
+    let title = node ? node.title : null;
+    let isBookmark = isItem && isURI;
+    let bulkTagging = !node;
+    let uris = bulkTagging ? aInitInfo.uris : null;
+    let visibleRows = new Set();
+    let isParentReadOnly = false;
+    if (node && "parent" in node) {
+      let parent = node.parent;
+      if (parent) {
+        isParentReadOnly = !PlacesUtils.nodeIsFolder(parent) ||
+                            PlacesUIUtils.isContentsReadOnly(parent);
+      }
+    }
+
+    return this._paneInfo = { itemId, itemGuid, isItem,
+                              isURI, uri, title,
+                              isBookmark, isFolderShortcut, isParentReadOnly,
+                              bulkTagging, uris,
+                              visibleRows };
+  },
+
+  get initialized() {
+    return this._paneInfo != null;
+  },
+
+  // Backwards-compatibility getters
+  get itemId() {
+    if (!this.initialized || this._paneInfo.bulkTagging)
+      return -1;
+    return this._paneInfo.itemId;
+  },
+
+  get uri() {
+    if (!this.initialized)
+      return null;
+    if (this._paneInfo.bulkTagging)
+      return this._paneInfo.uris[0];
+    return this._paneInfo.uri;
+  },
+
+  get multiEdit() {
+    return this.initialized && this._paneInfo.bulkTagging;
+  },
+
+  // Check if the pane is initialized to show only read-only fields.
+  get readOnly() {
+    // Bug 1120314 - Folder shortcuts are read-only due to some quirky implementation
+    // details (the most important being the "smart" semantics of node.title).
+    return (!this.initialized ||
+            (!this._paneInfo.visibleRows.has("tagsRow") &&
+             (this._paneInfo.isFolderShortcut ||
+              this._paneInfo.isParentReadOnly)));
+  },
 
   // the first field which was edited after this panel was initialized for
   // a certain item
   _firstEditedField: "",
 
-  get itemId() {
-    return this._itemId;
-  },
+  _initNamePicker() {
+    if (this._paneInfo.bulkTagging)
+      throw new Error("_initNamePicker called unexpectedly");
 
-  get uri() {
-    return this._uri;
-  },
-
-  get multiEdit() {
-    return this._multiEdit;
+    // title may by null, which, for us, is the same as an empty string.
+    this._initTextField(this._namePicker, this._paneInfo.title || "");
   },
 
-  /**
-   * Determines the initial data for the item edited or added by this dialog
-   */
-  _determineInfo: function EIO__determineInfo(aInfo) {
-    // hidden rows
-    if (aInfo && aInfo.hiddenRows)
-      this._hiddenRows = aInfo.hiddenRows;
-    else
-      this._hiddenRows.splice(0, this._hiddenRows.length);
-    // force-read-only
-    this._readOnly = aInfo && aInfo.forceReadOnly;
-    this._titleOverride = aInfo && aInfo.titleOverride ? aInfo.titleOverride
-                                                       : "";
+  _initLocationField() {
+    if (!this._paneInfo.isURI)
+      throw new Error("_initLocationField called unexpectedly");
+    this._initTextField(this._locationField, this._paneInfo.uri.spec);
+  },
+
+  _initDescriptionField() {
+    if (!this._paneInfo.isItem)
+      throw new Error("_initDescriptionField called unexpectedly");
+
+    this._initTextField(this._descriptionField,
+                        PlacesUIUtils.getItemDescription(this._paneInfo.itemId));
   },
 
-  _showHideRows: function EIO__showHideRows() {
-    var isBookmark = this._itemId != -1 &&
-                     this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK;
-    var isQuery = false;
-    if (this._uri)
-      isQuery = this._uri.schemeIs("place");
+  _initKeywordField: Task.async(function* (aNewKeyword) {
+    if (!this._paneInfo.isBookmark)
+      throw new Error("_initKeywordField called unexpectedly");
+
+    let newKeyword = aNewKeyword;
+    if (newKeyword === undefined) {
+      let itemId = this._paneInfo.itemId;
+      newKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId);
+    }
+    this._initTextField(this._keywordField, newKeyword);
+  }),
+
+  _initLoadInSidebar: Task.async(function* () {
+    if (!this._paneInfo.isBookmark)
+      throw new Error("_initLoadInSidebar called unexpectedly");
+
+    this._loadInSidebarCheckbox.checked =
+      PlacesUtils.annotations.itemHasAnnotation(
+        this._paneInfo.itemId, PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
+  }),
 
-    this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1;
-    this._element("folderRow").collapsed =
-      this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly;
-    this._element("tagsRow").collapsed = !this._uri ||
-      this._hiddenRows.indexOf("tags") != -1 || isQuery;
+  /**
+   * Initialize the panel.
+   *
+   * @param aInfo
+   *        An object having:
+   *        1. one of the following properties:
+   *        - node: either a result node or a node-like object representing the
+   *          item to be edited. A node-like object must have the following
+   *          properties (with values that match exactly those a result node
+   *          would have): itemId, bookmarkGuid, uri, title, type.
+   *        - uris: an array of uris for bulk tagging.
+   *
+   *        2. any of the following optional properties:
+   *          - hiddenRows (Strings array): list of rows to be hidden regardless
+   *            of the item edited. Possible values: "title", "location",
+   *            "description", "keyword", "loadInSidebar", "feedLocation",
+   *            "siteLocation", folderPicker"
+   */
+  initPanel(aInfo) {
+    if (typeof(aInfo) != "object" || aInfo === null)
+      throw new Error("aInfo must be an object.");
+
+    // For sanity ensure that the implementer has uninited the panel before
+    // trying to init it again, or we could end up leaking due to observers.
+    if (this.initialized)
+      this.uninitPanel(false);
+
+    let { itemId, itemGuid, isItem,
+          isURI, uri, title,
+          isBookmark, bulkTagging, uris,
+          visibleRows } = this._setPaneInfo(aInfo);
+
+    let showOrCollapse =
+      (rowId, isAppropriateForInput, nameInHiddenRows = null) => {
+        let visible = isAppropriateForInput;
+        if (visible && "hiddenRows" in aInfo && nameInHiddenRows)
+          visible &= aInfo.hiddenRows.indexOf(nameInHiddenRows) == -1;
+        if (visible)
+          visibleRows.add(rowId);
+        return !(this._element(rowId).collapsed = !visible);
+      };
+
+    if (showOrCollapse("nameRow", !bulkTagging, "name")) {
+      this._initNamePicker();
+      this._namePicker.readOnly = this.readOnly;
+    }
+
+    if (showOrCollapse("locationRow", isURI, "location")) {
+      this._initLocationField();
+      this._locationField.readOnly = !this._paneInfo.isItem;
+    }
+
+    if (showOrCollapse("descriptionRow",
+                       this._paneInfo.isItem && !this.readOnly,
+                       "description")) {
+      this._initDescriptionField();
+    }
+
+    if (showOrCollapse("keywordRow", isBookmark, "keyword"))
+      this._initKeywordField();
+
     // Collapse the tag selector if the item does not accept tags.
-    if (!this._element("tagsSelectorRow").collapsed &&
-        this._element("tagsRow").collapsed)
-      this.toggleTagsSelector();
-    this._element("descriptionRow").collapsed =
-      this._hiddenRows.indexOf("description") != -1 || this._readOnly;
-    this._element("keywordRow").collapsed = !isBookmark || this._readOnly ||
-      this._hiddenRows.indexOf("keyword") != -1 || isQuery;
-    this._element("locationRow").collapsed = !(this._uri && !isQuery) ||
-      this._hiddenRows.indexOf("location") != -1;
-    this._element("loadInSidebarCheckbox").collapsed = !isBookmark || isQuery ||
-      this._readOnly || this._hiddenRows.indexOf("loadInSidebar") != -1;
-    this._element("feedLocationRow").collapsed = !this._isLivemark ||
-      this._hiddenRows.indexOf("feedLocation") != -1;
-    this._element("siteLocationRow").collapsed = !this._isLivemark ||
-      this._hiddenRows.indexOf("siteLocation") != -1;
-    this._element("selectionCount").hidden = !this._multiEdit;
+    if (showOrCollapse("tagsRow", isURI || bulkTagging, "tags"))
+      this._initTagsField().catch(Components.utils.reportError);
+    else if (!this._element("tagsSelectorRow").collapsed)
+      this.toggleTagsSelector().catch(Components.utils.reportError);
+
+    // Load in sidebar.
+    if (showOrCollapse("loadInSidebarCheckbox", isBookmark, "loadInSidebar")) {
+      this._initLoadInSidebar();
+    }
+
+    // Folder picker.
+    // Technically we should check that the item is not moveable, but that's
+    // not cheap (we don't always have the parent), and there's no use case for
+    // this (it's only the Star UI that shows the folderPicker)
+    if (showOrCollapse("folderRow", isItem, "folderPicker")) {
+      let containerId = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
+      this._initFolderMenuList(containerId);
+    }
+
+    // Selection count.
+    if (showOrCollapse("selectionCount", bulkTagging)) {
+      this._element("itemsCountText").value =
+        PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+                                      uris.length,
+                                      [uris.length]);
+    }
+
+    // Observe changes.
+    if (!this._observersAdded) {
+      PlacesUtils.bookmarks.addObserver(this, false);
+      window.addEventListener("unload", this, false);
+      this._observersAdded = true;
+    }
   },
 
   /**
-   * Initialize the panel
-   * @param aFor
-   *        Either a places-itemId (of a bookmark, folder or a live bookmark),
-   *        an array of itemIds (used for bulk tagging), or a URI object (in 
-   *        which case, the panel would be initialized in read-only mode).
-   * @param [optional] aInfo
-   *        JS object which stores additional info for the panel
-   *        initialization. The following properties may bet set:
-   *        * hiddenRows (Strings array): list of rows to be hidden regardless
-   *          of the item edited. Possible values: "title", "location",
-   *          "description", "keyword", "loadInSidebar", "feedLocation",
-   *          "siteLocation", folderPicker"
-   *        * forceReadOnly - set this flag to initialize the panel to its
-   *          read-only (view) mode even if the given item is editable.
+   * Finds tags that are in common among this._currentInfo.uris;
    */
-  initPanel: function EIO_initPanel(aFor, aInfo) {
-    // For sanity ensure that the implementer has uninited the panel before
-    // trying to init it again, or we could end up leaking due to observers.
-    if (this._initialized)
-      this.uninitPanel(false);
+  _getCommonTags() {
+    if ("_cachedCommonTags" in this._paneInfo)
+      return this._paneInfo._cachedCommonTags;
 
-    var aItemIdList;
-    if (Array.isArray(aFor)) {
-      aItemIdList = aFor;
-      aFor = aItemIdList[0];
-    }
-    else if (this._multiEdit) {
-      this._multiEdit = false;
-      this._tags = [];
-      this._uris = [];
-      this._allTags = [];
-      this._itemIds = [];
-      this._element("selectionCount").hidden = true;
-    }
-
-    this._folderMenuList = this._element("folderMenuList");
-    this._folderTree = this._element("folderTree");
+    let uris = [...this._paneInfo.uris];
+    let firstURI = uris.shift();
+    let commonTags = new Set(PlacesUtils.tagging.getTagsForURI(firstURI));
+    if (commonTags.size == 0)
+      return this._cachedCommonTags = [];
 
-    this._determineInfo(aInfo);
-    if (aFor instanceof Ci.nsIURI) {
-      this._itemId = -1;
-      this._uri = aFor;
-      this._readOnly = true;
-    }
-    else {
-      this._itemId = aFor;
-      // We can't store information on invalid itemIds.
-      this._readOnly = this._readOnly || this._itemId == -1;
-
-      var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
-      this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId);
-      if (this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
-        this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
-        this._initTextField("keywordField",
-                            PlacesUtils.bookmarks
-                                       .getKeywordForBookmark(this._itemId));
-        this._element("loadInSidebarCheckbox").checked =
-          PlacesUtils.annotations.itemHasAnnotation(this._itemId,
-                                                    PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
+    for (let uri of uris) {
+      let curentURITags = PlacesUtils.tagging.getTagsForURI(uri);
+      for (let tag of commonTags) {
+        if (curentURITags.indexOf(tag) == -1) {
+          commonTags.delete(tag)
+          if (commonTags.size == 0)
+            return this._paneInfo.cachedCommonTags = [];
+        }
       }
-      else {
-        this._uri = null;
-        this._isLivemark = false;
-        PlacesUtils.livemarks.getLivemark({id: this._itemId })
-          .then(aLivemark => {
-            this._isLivemark = true;
-            this._initTextField("feedLocationField", aLivemark.feedURI.spec, true);
-            this._initTextField("siteLocationField", aLivemark.siteURI ? aLivemark.siteURI.spec : "", true);
-            this._showHideRows();
-          }, () => undefined);
-      }
-
-      // folder picker
-      this._initFolderMenuList(containerId);
-
-      // description field
-      this._initTextField("descriptionField", 
-                          PlacesUIUtils.getItemDescription(this._itemId));
     }
-
-    if (this._itemId == -1 ||
-        this._itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
-      this._isLivemark = false;
-
-      this._initTextField("locationField", this._uri.spec);
-      if (!aItemIdList) {
-        var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
-        this._initTextField("tagsField", tags, false);
-      }
-      else {
-        this._multiEdit = true;
-        this._allTags = [];
-        this._itemIds = aItemIdList;
-        for (var i = 0; i < aItemIdList.length; i++) {
-          if (aItemIdList[i] instanceof Ci.nsIURI) {
-            this._uris[i] = aItemIdList[i];
-            this._itemIds[i] = -1;
-          }
-          else
-            this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i]);
-          this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]);
-        }
-        this._allTags = this._getCommonTags();
-        this._initTextField("tagsField", this._allTags.join(", "), false);
-        this._element("itemsCountText").value =
-          PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
-                                        this._itemIds.length,
-                                        [this._itemIds.length]);
-      }
-
-      // tags selector
-      this._rebuildTagsSelectorList();
-    }
-
-    // name picker
-    this._initNamePicker();
-    
-    this._showHideRows();
-
-    // observe changes
-    if (!this._observersAdded) {
-      // Single bookmarks observe any change.  History entries and multiEdit
-      // observe only tags changes, through bookmarks.
-      if (this._itemId != -1 || this._uri || this._multiEdit)
-        PlacesUtils.bookmarks.addObserver(this, false);
-
-      this._element("namePicker").addEventListener("blur", this);
-      this._element("locationField").addEventListener("blur", this);
-      this._element("tagsField").addEventListener("blur", this);
-      this._element("keywordField").addEventListener("blur", this);
-      this._element("descriptionField").addEventListener("blur", this);
-      window.addEventListener("unload", this, false);
-      this._observersAdded = true;
-    }
-
-    this._initialized = true;
+    return this._paneInfo._cachedCommonTags = [...commonTags];
   },
 
-  /**
-   * Finds tags that are in common among this._tags entries that track tags
-   * for each selected uri.
-   * The tags arrays should be kept up-to-date for this to work properly.
-   *
-   * @return array of common tags for the selected uris.
-   */
-  _getCommonTags: function() {
-    return this._tags[0].filter(
-      function (aTag) this._tags.every(
-        function (aTags) aTags.indexOf(aTag) != -1
-      ), this
-    );
-  },
+  _initTextField(aElement, aValue) {
+    if (aElement.value != aValue) {
+      aElement.value = aValue;
 
-  _initTextField: function(aTextFieldId, aValue, aReadOnly) {
-    var field = this._element(aTextFieldId);
-    field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly;
-
-    if (field.value != aValue) {
-      field.value = aValue;
-
-      // clear the undo stack
-      var editor = field.editor;
+      // Clear the undo stack
+      let editor = aElement.editor;
       if (editor)
         editor.transactionManager.clear();
     }
   },
 
   /**
    * Appends a menu-item representing a bookmarks folder to a menu-popup.
    * @param aMenupopup
    *        The popup to which the menu-item should be added.
    * @param aFolderId
    *        The identifier of the bookmarks folder.
    * @return the new menu item.
    */
-  _appendFolderItemToMenupopup:
-  function EIO__appendFolderItemToMenuList(aMenupopup, aFolderId) {
+  _appendFolderItemToMenupopup(aMenupopup, aFolderId) {
     // First make sure the folders-separator is visible
     this._element("foldersSeparator").hidden = false;
 
     var folderMenuItem = document.createElement("menuitem");
     var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId)
     folderMenuItem.folderId = aFolderId;
     folderMenuItem.setAttribute("label", folderTitle);
     folderMenuItem.className = "menuitem-iconic folder-icon";
@@ -334,282 +353,266 @@ var gEditItemOverlay = {
     this._folderMenuList.setAttribute("selectedIndex",
                                       this._folderMenuList.selectedIndex);
 
     // Hide the folders-separator if no folder is annotated as recently-used
     this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6);
     this._folderMenuList.disabled = this._readOnly;
   },
 
-  QueryInterface: function EIO_QueryInterface(aIID) {
-    if (aIID.equals(Ci.nsIDOMEventListener) ||
-        aIID.equals(Ci.nsINavBookmarkObserver) ||
-        aIID.equals(Ci.nsISupports))
-      return this;
-
-    throw Cr.NS_ERROR_NO_INTERFACE;
-  },
-
-  _element: function EIO__element(aID) {
-    return document.getElementById("editBMPanel_" + aID);
-  },
-
-  _getItemStaticTitle: function EIO__getItemStaticTitle() {
-    if (this._titleOverride)
-      return this._titleOverride;
+  QueryInterface:
+  XPCOMUtils.generateQI([Components.interfaces.nsIDOMEventListener,
+                         Components.interfaces.nsINavBookmarkObserver]),
 
-    let title = "";
-    if (this._itemId == -1) {
-      title = PlacesUtils.history.getPageTitle(this._uri);
-    }
-    else {
-      title = PlacesUtils.bookmarks.getItemTitle(this._itemId);
-    }
-    return title;
-  },
+  _element(aID) document.getElementById("editBMPanel_" + aID),
 
-  _initNamePicker: function EIO_initNamePicker() {
-    var namePicker = this._element("namePicker");
-    namePicker.value = this._getItemStaticTitle();
-    namePicker.readOnly = this._readOnly;
-
-    // clear the undo stack
-    var editor = namePicker.editor;
-    if (editor)
-      editor.transactionManager.clear();
-  },
-
-  uninitPanel: function EIO_uninitPanel(aHideCollapsibleElements) {
+  uninitPanel(aHideCollapsibleElements) {
     if (aHideCollapsibleElements) {
-      // hide the folder tree if it was previously visible
+      // Hide the folder tree if it was previously visible.
       var folderTreeRow = this._element("folderTreeRow");
       if (!folderTreeRow.collapsed)
         this.toggleFolderTreeVisibility();
 
-      // hide the tag selector if it was previously visible
+      // Hide the tag selector if it was previously visible.
       var tagsSelectorRow = this._element("tagsSelectorRow");
       if (!tagsSelectorRow.collapsed)
         this.toggleTagsSelector();
     }
 
     if (this._observersAdded) {
-      if (this._itemId != -1 || this._uri || this._multiEdit)
-        PlacesUtils.bookmarks.removeObserver(this);
-
-      this._element("namePicker").removeEventListener("blur", this);
-      this._element("locationField").removeEventListener("blur", this);
-      this._element("tagsField").removeEventListener("blur", this);
-      this._element("keywordField").removeEventListener("blur", this);
-      this._element("descriptionField").removeEventListener("blur", this);
-
+      PlacesUtils.bookmarks.removeObserver(this);
       this._observersAdded = false;
     }
 
-    this._itemId = -1;
-    this._uri = null;
-    this._uris = [];
-    this._tags = [];
-    this._allTags = [];
-    this._itemIds = [];
-    this._multiEdit = false;
+    this._setPaneInfo(null);
     this._firstEditedField = "";
-    this._initialized = false;
-    this._titleOverride = "";
-    this._readOnly = false;
+  },
+
+  onTagsFieldChange() {
+    if (!this.readOnly) {
+      this._updateTags().then(
+        anyChanges => {
+          if (anyChanges)
+            this._mayUpdateFirstEditField("tagsField");
+        }, Components.utils.reportError);
+    }
   },
 
-  onTagsFieldBlur: function EIO_onTagsFieldBlur() {
-    if (this._updateTags()) // if anything has changed
-      this._mayUpdateFirstEditField("tagsField");
-  },
+  /**
+   * For a given array of currently-set tags and the tags-input-field
+   * value, returns which tags should be removed and which should be added in
+   * the form of { removedTags: [...], newTags: [...] }.
+   */
+  _getTagsChanges(aCurrentTags) {
+    let inputTags = this._getTagsArrayFromTagsInputField();
 
-  _updateTags: function EIO__updateTags() {
-    if (this._multiEdit)
-      return this._updateMultipleTagsForItems();
-    return this._updateSingleTagForItem();
+    // Optimize the trivial cases (which are actually the most common).
+    if (inputTags.length == 0 && aCurrentTags.length == 0)
+      return { newTags: [], removedTags: [] };
+    if (inputTags.length == 0)
+      return { newTags: [], removedTags: aCurrentTags };
+    if (aCurrentTags.length == 0)
+      return { newTags: inputTags, removedTags: [] };
+
+    let removedTags = aCurrentTags.filter(t => inputTags.indexOf(t) == -1);
+    let newTags = inputTags.filter(t => aCurrentTags.indexOf(t) == -1);
+    return { removedTags, newTags };
   },
 
-  _updateSingleTagForItem: function EIO__updateSingleTagForItem() {
-    var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri);
-    var tags = this._getTagsArrayFromTagField();
-    if (tags.length > 0 || currentTags.length > 0) {
-      var tagsToRemove = [];
-      var tagsToAdd = [];
-      var txns = []; 
-      for (var i = 0; i < currentTags.length; i++) {
-        if (tags.indexOf(currentTags[i]) == -1)
-          tagsToRemove.push(currentTags[i]);
-      }
-      for (var i = 0; i < tags.length; i++) {
-        if (currentTags.indexOf(tags[i]) == -1)
-          tagsToAdd.push(tags[i]);
+  // Adds and removes tags for one or more uris.
+  _setTagsFromTagsInputField: Task.async(function* (aCurrentTags, aURIs) {
+    let { removedTags, newTags } = this._getTagsChanges(aCurrentTags);
+    if (removedTags.length + newTags.length == 0)
+      return false;
+
+    if (!PlacesUIUtils.useAsyncTransactions) {
+      let txns = [];
+      for (let uri of aURIs) {
+        if (removedTags.length > 0)
+          txns.push(new PlacesUntagURITransaction(uri, removedTags));
+        if (newTags.length > 0)
+          txns.push(new PlacesTagURITransaction(uri, newTags));
       }
 
-      if (tagsToRemove.length > 0) {
-        let untagTxn = new PlacesUntagURITransaction(this._uri, tagsToRemove);
-        txns.push(untagTxn);
+      PlacesUtils.transactionManager.doTransaction(
+        new PlacesAggregatedTransaction("Update tags", txns));
+      return true;
+    }
+
+    let setTags = function* () {
+      if (newTags.length > 0) {
+        yield PlacesTransactions.Tag({ urls: aURIs, tags: newTags })
+                                .transact();
       }
-      if (tagsToAdd.length > 0) {
-        let tagTxn = new PlacesTagURITransaction(this._uri, tagsToAdd);
-        txns.push(tagTxn);
+      if (removedTags.length > 0) {
+        yield PlacesTransactions.Untag({ urls: aURIs, tags: removedTags })
+                          .transact();
       }
+    };
 
-      if (txns.length > 0) {
-        let aggregate = new PlacesAggregatedTransaction("Update tags", txns);
-        PlacesUtils.transactionManager.doTransaction(aggregate);
+    // Only in the library info-pane it's safe (and necessary) to batch these.
+    // TODO bug 1093030: cleanup this mess when the bookmarksProperties dialog
+    // and star UI code don't "run a batch in the background".
+    if (window.document.documentElement.id == "places")
+      PlacesTransactions.batch(setTags).catch(Components.utils.reportError);
+    else
+      Task.spawn(setTags).catch(Components.utils.reportError);
+    return true;
+  }),
 
-        // Ensure the tagsField is in sync, clean it up from empty tags
-        var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
-        this._initTextField("tagsField", tags, false);
-        return true;
-      }
-    }
-    return false;
-  },
+  _updateTags: Task.async(function*() {
+    let uris = this._paneInfo.bulkTagging ?
+                 this._paneInfo.uris : [this._paneInfo.uri];
+    let currentTags = this._paneInfo.bulkTagging ?
+                        yield this._getCommonTags() :
+                        PlacesUtils.tagging.getTagsForURI(uris[0]);
+    let anyChanges = yield this._setTagsFromTagsInputField(currentTags, uris);
+    if (!anyChanges)
+      return false;
 
-   /**
-    * Stores the first-edit field for this dialog, if the passed-in field
-    * is indeed the first edited field
-    * @param aNewField
-    *        the id of the field that may be set (without the "editBMPanel_"
-    *        prefix)
-    */
-  _mayUpdateFirstEditField: function EIO__mayUpdateFirstEditField(aNewField) {
+    // Ensure the tagsField is in sync, clean it up from empty tags
+    currentTags = this._paneInfo.bulkTagging ?
+                    this._getCommonTags() :
+                    PlacesUtils.tagging.getTagsForURI(this._uri);
+    this._initTextField(this._tagsField, currentTags.join(", "), false);
+    return true;
+  }),
+
+  /**
+   * Stores the first-edit field for this dialog, if the passed-in field
+   * is indeed the first edited field
+   * @param aNewField
+   *        the id of the field that may be set (without the "editBMPanel_"
+   *        prefix)
+   */
+  _mayUpdateFirstEditField(aNewField) {
     // * The first-edit-field behavior is not applied in the multi-edit case
     // * if this._firstEditedField is already set, this is not the first field,
     //   so there's nothing to do
-    if (this._multiEdit || this._firstEditedField)
+    if (this._paneInfo.bulkTagging || this._firstEditedField)
       return;
 
     this._firstEditedField = aNewField;
 
     // set the pref
     var prefs = Cc["@mozilla.org/preferences-service;1"].
                 getService(Ci.nsIPrefBranch);
     prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField);
   },
 
-  _updateMultipleTagsForItems: function EIO__updateMultipleTagsForItems() {
-    var tags = this._getTagsArrayFromTagField();
-    if (tags.length > 0 || this._allTags.length > 0) {
-      var tagsToRemove = [];
-      var tagsToAdd = [];
-      var txns = []; 
-      for (var i = 0; i < this._allTags.length; i++) {
-        if (tags.indexOf(this._allTags[i]) == -1)
-          tagsToRemove.push(this._allTags[i]);
-      }
-      for (var i = 0; i < this._tags.length; i++) {
-        tagsToAdd[i] = [];
-        for (var j = 0; j < tags.length; j++) {
-          if (this._tags[i].indexOf(tags[j]) == -1)
-            tagsToAdd[i].push(tags[j]);
-        }
-      }
-
-      if (tagsToAdd.length > 0) {
-        for (let i = 0; i < this._uris.length; i++) {
-          if (tagsToAdd[i].length > 0) {
-            let tagTxn = new PlacesTagURITransaction(this._uris[i],
-                                                     tagsToAdd[i]);
-            txns.push(tagTxn);
-          }
-        }
-      }
-      if (tagsToRemove.length > 0) {
-        for (let i = 0; i < this._uris.length; i++) {
-          let untagTxn = new PlacesUntagURITransaction(this._uris[i],
-                                                       tagsToRemove);
-          txns.push(untagTxn);
-        }
-      }
-
-      if (txns.length > 0) {
-        let aggregate = new PlacesAggregatedTransaction("Update tags", txns);
-        PlacesUtils.transactionManager.doTransaction(aggregate);
-
-        this._allTags = tags;
-        this._tags = [];
-        for (let i = 0; i < this._uris.length; i++) {
-          this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]);
-        }
-
-        // Ensure the tagsField is in sync, clean it up from empty tags
-        this._initTextField("tagsField", tags, false);
-        return true;
-      }
-    }
-    return false;
-  },
-
-  onNamePickerBlur: function EIO_onNamePickerBlur() {
-    if (this._itemId == -1)
+  onNamePickerChange() {
+    if (this.readOnly || !this._paneInfo.isItem)
       return;
 
-    var namePicker = this._element("namePicker")
-
     // Here we update either the item title or its cached static title
-    var newTitle = namePicker.value;
+    let newTitle = this._namePicker.value;
     if (!newTitle &&
-        PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) == PlacesUtils.tagsFolderId) {
+        PlacesUtils.bookmarks.getFolderIdForItem(itemId) == PlacesUtils.tagsFolderId) {
       // We don't allow setting an empty title for a tag, restore the old one.
       this._initNamePicker();
     }
-    else if (this._getItemStaticTitle() != newTitle) {
+    else {
       this._mayUpdateFirstEditField("namePicker");
-      let txn = new PlacesEditItemTitleTransaction(this._itemId, newTitle);
-      PlacesUtils.transactionManager.doTransaction(txn);
+      if (!PlacesUIUtils.useAsyncTransactions) {
+        let txn = new PlacesEditItemTitleTransaction(this._paneInfo.itemId,
+                                                     newTitle);
+        PlacesUtils.transactionManager.doTransaction(txn);
+        return;
+      }
+      let guid = this._paneInfo.itemGuid;
+      PlacesTransactions.EditTitle({ guid, title: newTitle })
+                        .transact().catch(Components.utils.reportError);
     }
   },
 
-  onDescriptionFieldBlur: function EIO_onDescriptionFieldBlur() {
-    var description = this._element("descriptionField").value;
+  onDescriptionFieldChange() {
+    if (this.readOnly || !this._paneInfo.isItem)
+      return;
+
+    let itemId = this._paneInfo.itemId;
+    let description = this._element("descriptionField").value;
     if (description != PlacesUIUtils.getItemDescription(this._itemId)) {
-      var annoObj = { name   : PlacesUIUtils.DESCRIPTION_ANNO,
-                      type   : Ci.nsIAnnotationService.TYPE_STRING,
-                      flags  : 0,
-                      value  : description,
-                      expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
-      var txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj);
-      PlacesUtils.transactionManager.doTransaction(txn);
+      let annotation =
+        { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
+      if (!PlacesUIUtils.useAsyncTransactions) {
+        let txn = new PlacesSetItemAnnotationTransaction(itemId,
+                                                         annotation);
+        PlacesUtils.transactionManager.doTransaction(txn);
+        return;
+      }
+      let guid = this._paneInfo.itemGuid;
+      PlacesTransactions.Annotate({ guid, annotation })
+                        .transact().catch(Components.utils.reportError);
     }
   },
 
-  onLocationFieldBlur: function EIO_onLocationFieldBlur() {
-    var uri;
+  onLocationFieldChange() {
+    if (this.readOnly || !this._paneInfo.isBookmark)
+      return;
+
+    let newURI;
     try {
-      uri = PlacesUIUtils.createFixedURI(this._element("locationField").value);
+      newURI = PlacesUIUtils.createFixedURI(this._locationField.value);
+    }
+    catch(ex) {
+      // TODO: Bug 1089141 - Provide some feedback about the invalid url.
+      return;
     }
-    catch(ex) { return; }
+
+    if (this._paneInfo.uri.equals(newURI))
+      return;
 
-    if (!this._uri.equals(uri)) {
-      var txn = new PlacesEditBookmarkURITransaction(this._itemId, uri);
+    if (!PlacesUIUtils.useAsyncTransactions) {
+      let itemId = this._paneInfo.itemId;
+      let txn = new PlacesEditBookmarkURITransaction(this._itemId, newURI);
       PlacesUtils.transactionManager.doTransaction(txn);
-      this._uri = uri;
+      return;
     }
+    let guid = this._paneInfo.itemGuid;
+    PlacesTransactions.EditUrl({ guid, url: newURI })
+                      .transact().catch(Components.utils.reportError);
   },
 
-  onKeywordFieldBlur: function EIO_onKeywordFieldBlur() {
-    var keyword = this._element("keywordField").value;
-    if (keyword != PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId)) {
-      var txn = new PlacesEditBookmarkKeywordTransaction(this._itemId, keyword);
+  onKeywordFieldChange() {
+    if (this.readOnly || !this._paneInfo.isBookmark)
+      return;
+
+    let itemId = this._paneInfo.itemId;
+    let newKeyword = this._keywordField.value;
+    if (!PlacesUIUtils.useAsyncTransactions) {
+      let txn = new PlacesEditBookmarkKeywordTransaction(itemId, newKeyword);
       PlacesUtils.transactionManager.doTransaction(txn);
+      return;
     }
+    let guid = this._paneInfo.itemGuid;
+    PlacesTransactions.EditKeyword({ guid, keyword: newKeyword })
+                      .transact().catch(Components.utils.reportError);
   },
 
-  onLoadInSidebarCheckboxCommand:
-  function EIO_onLoadInSidebarCheckboxCommand() {
-    let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO };
-    if (this._element("loadInSidebarCheckbox").checked)
-      annoObj.value = true;
-    let txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj);
-    PlacesUtils.transactionManager.doTransaction(txn);
+  onLoadInSidebarCheckboxCommand() {
+    if (!this.initialized || !this._paneInfo.isBookmark)
+      return;
+
+    let annotation = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO };
+    if (this._loadInSidebarCheckbox.checked)
+      annotation.value = true;
+
+    if (!PlacesUIUtils.useAsyncTransactions) {
+      let itemId = this._paneInfo.itemId;
+      let txn = new PlacesSetItemAnnotationTransaction(itemId,
+                                                       annotation);
+      PlacesUtils.transactionManager.doTransaction(txn);
+      return;
+    }
+    let guid = this._paneInfo.itemGuid;
+    PlacesTransactions.Annotate({ guid, annotation })
+                      .transact().catch(Components.utils.reportError);
   },
 
-  toggleFolderTreeVisibility: function EIO_toggleFolderTreeVisibility() {
+  toggleFolderTreeVisibility() {
     var expander = this._element("foldersExpander");
     var folderTreeRow = this._element("folderTreeRow");
     if (!folderTreeRow.collapsed) {
       expander.className = "expander-down";
       expander.setAttribute("tooltiptext",
                             expander.getAttribute("tooltiptextdown"));
       folderTreeRow.collapsed = true;
       this._element("chooseFolderSeparator").hidden =
@@ -632,93 +635,100 @@ var gEditItemOverlay = {
       this._element("chooseFolderSeparator").hidden =
         this._element("chooseFolderMenuItem").hidden = true;
       var currentFolder = this._getFolderIdFromMenuList();
       this._folderTree.selectItems([currentFolder]);
       this._folderTree.focus();
     }
   },
 
-  _getFolderIdFromMenuList:
-  function EIO__getFolderIdFromMenuList() {
+  _getFolderIdFromMenuList() {
     var selectedItem = this._folderMenuList.selectedItem;
     NS_ASSERT("folderId" in selectedItem,
               "Invalid menuitem in the folders-menulist");
     return selectedItem.folderId;
   },
 
   /**
    * Get the corresponding menu-item in the folder-menu-list for a bookmarks
    * folder if such an item exists. Otherwise, this creates a menu-item for the
    * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached,
    * the new item replaces the last menu-item.
    * @param aFolderId
    *        The identifier of the bookmarks folder.
    */
-  _getFolderMenuItem:
-  function EIO__getFolderMenuItem(aFolderId) {
-    var menupopup = this._folderMenuList.menupopup;
-
-    for (let i = 0; i < menupopup.childNodes.length; i++) {
-      if ("folderId" in menupopup.childNodes[i] &&
-          menupopup.childNodes[i].folderId == aFolderId)
-        return menupopup.childNodes[i];
-    }
+  _getFolderMenuItem(aFolderId) {
+    let menuPopup = this._folderMenuList.menupopup;
+    let menuItem = Array.prototype.find.call(
+      menuPopup.childNodes, menuItem => menuItem.folderId === aFolderId);
+    if (menuItem !== undefined)
+      return menuItem;
 
     // 3 special folders + separator + folder-items-count limit
     if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST)
-      menupopup.removeChild(menupopup.lastChild);
+      menupopup.removeChild(menuPopup.lastChild);
 
-    return this._appendFolderItemToMenupopup(menupopup, aFolderId);
+    return this._appendFolderItemToMenupopup(menuPopup, aFolderId);
   },
 
-  onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) {
+  onFolderMenuListCommand(aEvent) {
     // Set a selectedIndex attribute to show special icons
     this._folderMenuList.setAttribute("selectedIndex",
                                       this._folderMenuList.selectedIndex);
 
     if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") {
       // reset the selection back to where it was and expand the tree
       // (this menu-item is hidden when the tree is already visible
-      var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
-      var item = this._getFolderMenuItem(container);
+      let containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
+      let item = this._getFolderMenuItem(containerId);
       this._folderMenuList.selectedItem = item;
       // XXXmano HACK: setTimeout 100, otherwise focus goes back to the
       // menulist right away
       setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this);
       return;
     }
 
     // Move the item
-    var container = this._getFolderIdFromMenuList();
-    if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) {
-      var txn = new PlacesMoveItemTransaction(this._itemId, 
-                                              container, 
-                                              PlacesUtils.bookmarks.DEFAULT_INDEX);
-      PlacesUtils.transactionManager.doTransaction(txn);
+    let containerId = this._getFolderIdFromMenuList();
+    if (PlacesUtils.bookmarks.getFolderIdForItem(this._paneInfo.itemId) != containerId) {
+      if (PlacesUIUtils.useAsyncTransactions) {
+        Task.spawn(function* () {
+          let newParentGuid = yield PlacesUtils.promiseItemGuid(containerId);
+          let guid = this._paneInfo.itemGuid;
+          yield PlacesTransactions.Move({ guid, newParentGuid }).transact();
+        }.bind(this));
+      }
+      else {
+        let txn = new PlacesMoveItemTransaction(this._itemId,
+                                                containerId,
+                                                PlacesUtils.bookmarks.DEFAULT_INDEX);
+        PlacesUtils.transactionManager.doTransaction(txn);
+      }
 
       // Mark the containing folder as recently-used if it isn't in the
       // static list
-      if (container != PlacesUtils.unfiledBookmarksFolderId &&
-          container != PlacesUtils.toolbarFolderId &&
-          container != PlacesUtils.bookmarksMenuFolderId)
-        this._markFolderAsRecentlyUsed(container);
+      if (containerId != PlacesUtils.unfiledBookmarksFolderId &&
+          containerId != PlacesUtils.toolbarFolderId &&
+          containerId != PlacesUtils.bookmarksMenuFolderId) {
+        this._markFolderAsRecentlyUsed(container)
+            .catch(Components.utils.reportError);
+      }
     }
 
     // Update folder-tree selection
     var folderTreeRow = this._element("folderTreeRow");
     if (!folderTreeRow.collapsed) {
       var selectedNode = this._folderTree.selectedNode;
       if (!selectedNode ||
           PlacesUtils.getConcreteItemId(selectedNode) != container)
         this._folderTree.selectItems([container]);
     }
   },
 
-  onFolderTreeSelect: function EIO_onFolderTreeSelect() {
+  onFolderTreeSelect() {
     var selectedNode = this._folderTree.selectedNode;
 
     // Disable the "New Folder" button if we cannot create a new folder
     this._element("newFolderButton")
         .disabled = !this._folderTree.insertionPoint || !selectedNode;
 
     if (!selectedNode)
       return;
@@ -727,77 +737,93 @@ var gEditItemOverlay = {
     if (this._getFolderIdFromMenuList() == folderId)
       return;
 
     var folderItem = this._getFolderMenuItem(folderId);
     this._folderMenuList.selectedItem = folderItem;
     folderItem.doCommand();
   },
 
-  _markFolderAsRecentlyUsed:
-  function EIO__markFolderAsRecentlyUsed(aFolderId) {
-    var txns = [];
+  _markFolderAsRecentlyUsed: Task.async(function* (aFolderId) {
+    if (!PlacesUIUtils.useAsyncTransactions) {
+      let txns = [];
+
+      // Expire old unused recent folders.
+      let annotation = this._getLastUsedAnnotationObject(false);
+      while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
+        let folderId = this._recentFolders.pop().folderId;
+        let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno);
+        txns.push(annoTxn);
+      }
+
+      // Mark folder as recently used
+      annotation = this._getLastUsedAnnotationObject(true);
+      let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno);
+      txns.push(annoTxn);
 
-    // Expire old unused recent folders
-    var anno = this._getLastUsedAnnotationObject(false);
+      let aggregate =
+        new PlacesAggregatedTransaction("Update last used folders", txns);
+      PlacesUtils.transactionManager.doTransaction(aggregate);
+      return;
+    }
+
+    // Expire old unused recent folders.
+    let guids = [];
     while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
-      var folderId = this._recentFolders.pop().folderId;
-      let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno);
-      txns.push(annoTxn);
+      let folderId = this._recentFolders.pop().folderId;
+      let guid = yield PlacesUtils.promiseItemGuid(folderId);
+      guids.push(guid);
+    }
+    if (guids.length > 0) {
+      let annotation = this._getLastUsedAnnotationObject(false);
+      PlacesTransactions.Annotate({ guids, annotation  })
+                        .transact().catch(Components.utils.reportError);
     }
 
     // Mark folder as recently used
-    anno = this._getLastUsedAnnotationObject(true);
-    let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno);
-    txns.push(annoTxn);
-
-    let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns);
-    PlacesUtils.transactionManager.doTransaction(aggregate);
-  },
+    let annotation = this._getLastUsedAnnotationObject(true);
+    let guid = yield PlacesUtils.promiseItemGuid(aFolderId);
+    PlacesTransactions.Annotate({ guid, annotation })
+                      .transact().catch(Components.utils.reportError);
+  }),
 
   /**
    * Returns an object which could then be used to set/unset the
    * LAST_USED_ANNO annotation for a folder.
    *
    * @param aLastUsed
    *        Whether to set or unset the LAST_USED_ANNO annotation.
    * @returns an object representing the annotation which could then be used
    *          with the transaction manager.
    */
-  _getLastUsedAnnotationObject:
-  function EIO__getLastUsedAnnotationObject(aLastUsed) {
-    var anno = { name: LAST_USED_ANNO,
-                 type: Ci.nsIAnnotationService.TYPE_INT32,
-                 flags: 0,
-                 value: aLastUsed ? new Date().getTime() : null,
-                 expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
-
-    return anno;
+  _getLastUsedAnnotationObject(aLastUsed) {
+    return { name: LAST_USED_ANNO,
+             value: aLastUsed ? new Date().getTime() : null };
   },
 
-  _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() {
-    var tagsSelector = this._element("tagsSelector");
-    var tagsSelectorRow = this._element("tagsSelectorRow");
+  _rebuildTagsSelectorList: Task.async(function* () {
+    let tagsSelector = this._element("tagsSelector");
+    let tagsSelectorRow = this._element("tagsSelectorRow");
     if (tagsSelectorRow.collapsed)
       return;
 
     // Save the current scroll position and restore it after the rebuild.
     let firstIndex = tagsSelector.getIndexOfFirstVisibleRow();
     let selectedIndex = tagsSelector.selectedIndex;
     let selectedTag = selectedIndex >= 0 ? tagsSelector.selectedItem.label
                                          : null;
 
-    while (tagsSelector.hasChildNodes())
+    while (tagsSelector.hasChildNodes()) {
       tagsSelector.removeChild(tagsSelector.lastChild);
+    }
 
-    var tagsInField = this._getTagsArrayFromTagField();
-    var allTags = PlacesUtils.tagging.allTags;
-    for (var i = 0; i < allTags.length; i++) {
-      var tag = allTags[i];
-      var elt = document.createElement("listitem");
+    let tagsInField = this._getTagsArrayFromTagsInputField();
+    let allTags = PlacesUtils.tagging.allTags;
+    for (tag of allTags) {
+      let elt = document.createElement("listitem");
       elt.setAttribute("type", "checkbox");
       elt.setAttribute("label", tag);
       if (tagsInField.indexOf(tag) != -1)
         elt.setAttribute("checked", "true");
       tagsSelector.appendChild(elt);
       if (selectedTag === tag)
         selectedIndex = tagsSelector.getIndexOfItem(elt);
     }
@@ -809,238 +835,238 @@ var gEditItemOverlay = {
       Math.min(firstIndex,
                tagsSelector.itemCount - tagsSelector.getNumberOfVisibleRows());
     tagsSelector.scrollToIndex(firstIndex);
     if (selectedIndex >= 0 && tagsSelector.itemCount > 0) {
       selectedIndex = Math.min(selectedIndex, tagsSelector.itemCount - 1);
       tagsSelector.selectedIndex = selectedIndex;
       tagsSelector.ensureIndexIsVisible(selectedIndex);
     }
-  },
+  }),
 
-  toggleTagsSelector: function EIO_toggleTagsSelector() {
+  toggleTagsSelector: Task.async(function* () {
     var tagsSelector = this._element("tagsSelector");
     var tagsSelectorRow = this._element("tagsSelectorRow");
     var expander = this._element("tagsSelectorExpander");
     if (tagsSelectorRow.collapsed) {
       expander.className = "expander-up";
       expander.setAttribute("tooltiptext",
                             expander.getAttribute("tooltiptextup"));
       tagsSelectorRow.collapsed = false;
-      this._rebuildTagsSelectorList();
+      yield this._rebuildTagsSelectorList();
 
       // This is a no-op if we've added the listener.
       tagsSelector.addEventListener("CheckboxStateChange", this, false);
     }
     else {
       expander.className = "expander-down";
       expander.setAttribute("tooltiptext",
                             expander.getAttribute("tooltiptextdown"));
       tagsSelectorRow.collapsed = true;
     }
-  },
+  }),
 
   /**
    * Splits "tagsField" element value, returning an array of valid tag strings.
    *
    * @return Array of tag strings found in the field value.
    */
-  _getTagsArrayFromTagField: function EIO__getTagsArrayFromTagField() {
+  _getTagsArrayFromTagsInputField() {
     let tags = this._element("tagsField").value;
     return tags.trim()
                .split(/\s*,\s*/) // Split on commas and remove spaces.
                .filter(function (tag) tag.length > 0); // Kill empty tags.
   },
 
-  newFolder: function EIO_newFolder() {
-    var ip = this._folderTree.insertionPoint;
+  newFolder: Task.async(function* () {
+    let ip = this._folderTree.insertionPoint;
 
     // default to the bookmarks menu folder
     if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) {
-        ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
-                                PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                Ci.nsITreeView.DROP_ON);
+      ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
+                              PlacesUtils.bookmarks.DEFAULT_INDEX,
+                              Ci.nsITreeView.DROP_ON);
     }
 
     // XXXmano: add a separate "New Folder" string at some point...
-    var defaultLabel = this._element("newFolderButton").label;
-    var txn = new PlacesCreateFolderTransaction(defaultLabel, ip.itemId, ip.index);
-    PlacesUtils.transactionManager.doTransaction(txn);
+    let title = this._element("newFolderButton").label;
+    if (PlacesUIUtils.useAsyncTransactions) {
+      let parentGuid = yield ip.promiseGuid();
+      yield PlacesTransactions.NewFolder({ parentGuid, title, index: ip.index })
+                              .transact().catch(Components.utils.reportError);
+    }
+    else {
+      let txn = new PlacesCreateFolderTransaction(title, ip.itemId, ip.index);
+      PlacesUtils.transactionManager.doTransaction(txn);
+    }
+
     this._folderTree.focus();
     this._folderTree.selectItems([ip.itemId]);
     PlacesUtils.asContainer(this._folderTree.selectedNode).containerOpen = true;
     this._folderTree.selectItems([this._lastNewItem]);
     this._folderTree.startEditing(this._folderTree.view.selection.currentIndex,
                                   this._folderTree.columns.getFirstColumn());
-  },
+  }),
 
   // nsIDOMEventListener
-  handleEvent: function EIO_nsIDOMEventListener(aEvent) {
+  handleEvent(aEvent) {
     switch (aEvent.type) {
     case "CheckboxStateChange":
       // Update the tags field when items are checked/unchecked in the listbox
-      let tags = this._getTagsArrayFromTagField();
+      let tags = this._getTagsArrayFromTagsInputField();
       let tagCheckbox = aEvent.target;
 
       let curTagIndex = tags.indexOf(tagCheckbox.label);
-
       let tagsSelector = this._element("tagsSelector");
       tagsSelector.selectedItem = tagCheckbox;
 
       if (tagCheckbox.checked) {
         if (curTagIndex == -1)
           tags.push(tagCheckbox.label);
       }
       else {
         if (curTagIndex != -1)
           tags.splice(curTagIndex, 1);
       }
       this._element("tagsField").value = tags.join(", ");
       this._updateTags();
       break;
-    case "blur":
-      let replaceFn = (str, firstLetter) => firstLetter.toUpperCase();
-      let nodeName = aEvent.target.id.replace(/editBMPanel_(\w)/, replaceFn);
-      this["on" + nodeName + "Blur"]();
-      break;
     case "unload":
       this.uninitPanel(false);
       break;
     }
   },
 
-  // nsINavBookmarkObserver
-  onItemChanged: function EIO_onItemChanged(aItemId, aProperty,
-                                            aIsAnnotationProperty, aValue,
-                                            aLastModified, aItemType) {
-    if (aProperty == "tags") {
-      // Tags case is special, since they should be updated if either:
-      // - the notification is for the edited bookmark
-      // - the notification is for the edited history entry
-      // - the notification is for one of edited uris
-      let shouldUpdateTagsField = this._itemId == aItemId;
-      if (this._itemId == -1 || this._multiEdit) {
-        // Check if the changed uri is part of the modified ones.
+  _initTagsField: Task.async(function* () {
+    let tags;
+    if (this._paneInfo.isURI)
+      tags = PlacesUtils.tagging.getTagsForURI(this._paneInfo.uri);
+    else if (this._paneInfo.bulkTagging)
+      tags = this._getCommonTags();
+    else
+      throw new Error("_promiseTagsStr called unexpectedly");
+
+    this._initTextField(this._tagsField, tags.join(", "));
+  }),
+
+  _onTagsChange(aItemId) {
+    let paneInfo = this._paneInfo;
+    let updateTagsField = false;
+    if (paneInfo.isURI) {
+      if (paneInfo.isBookmark && aItemId == paneInfo.itemId) {
+        updateTagsField = true;
+      }
+      else if (!paneInfo.isBookmark) {
         let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
-        let uris = this._multiEdit ? this._uris : [this._uri];
-        uris.forEach(function (aURI, aIndex) {
-          if (aURI.equals(changedURI)) {
-            shouldUpdateTagsField = true;
-            if (this._multiEdit) {
-              this._tags[aIndex] = PlacesUtils.tagging.getTagsForURI(this._uris[aIndex]);
-            }
-          }
-        }, this);
+        updateTagsField = changedURI.equals(paneInfo.uri);
       }
-
-      if (shouldUpdateTagsField) {
-        if (this._multiEdit) {
-          this._allTags = this._getCommonTags();
-          this._initTextField("tagsField", this._allTags.join(", "), false);
-        }
-        else {
-          let tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
-          this._initTextField("tagsField", tags, false);
-        }
+    }
+    else if (paneInfo.bulkTagging) {
+      let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
+      if (paneInfo.uris.some(uri => uri.equals(changedURI))) {
+        updateTagsField = true;
+        delete this._paneInfo._cachedCommonTags;
       }
-
-      // Any tags change should be reflected in the tags selector.
-      this._rebuildTagsSelectorList();
-      return;
+    }
+    else {
+      throw new Error("_onTagsChange called unexpectedly");
     }
 
-    if (this._itemId != aItemId) {
-      if (aProperty == "title") {
-        // If the title of a folder which is listed within the folders
-        // menulist has been changed, we need to update the label of its
-        // representing element.
-        var menupopup = this._folderMenuList.menupopup;
-        for (let i = 0; i < menupopup.childNodes.length; i++) {
-          if ("folderId" in menupopup.childNodes[i] &&
-              menupopup.childNodes[i].folderId == aItemId) {
-            menupopup.childNodes[i].label = aValue;
-            break;
-          }
+    if (updateTagsField)
+      this._initTagsField().catch(Components.utils.reportError);
+
+    // Any tags change should be reflected in the tags selector.
+    if (this._element("tagsSelector"))
+      this._rebuildTagsSelectorList().catch(Components.utils.reportError);
+  },
+
+  _onItemTitleChange(aItemId, aNewTitle) {
+    if (!this._paneInfo.isBookmark)
+      return;
+    if (aItemId == this._paneInfo.itemId) {
+      this._paneInfo.title = aNewTitle;
+      this._initTextField(this._namePicker);
+    }
+    else if (this._paneInfo.visibleRows.has("folderRow")) {
+      // If the title of a folder which is listed within the folders
+      // menulist has been changed, we need to update the label of its
+      // representing element.
+      let menupopup = this._folderMenuList.menupopup;
+      for (menuitem of menupopup.childNodes) {
+        if ("folderId" in menuItem && menuItem.folderId == aItemId) {
+          menuitem.label = aNewTitle;
+          break;
         }
       }
+    }
+  },
 
+  // nsINavBookmarkObserver
+  onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aValue,
+                aLastModified, aItemType) {
+    if (aProperty == "tags" && this._paneInfo.visibleRows.has("tagsRow"))
+      this._onTagsChange(aItemId);
+    else if (this._paneInfo.isItem && aProperty == "title")
+      this._onItemTitleChange(aItemId, aValue);
+    else (!this._paneInfo.isItem || this._paneInfo.itemId != aItemId)
       return;
-    }
 
     switch (aProperty) {
-    case "title":
-      var namePicker = this._element("namePicker");
-      if (namePicker.value != aValue) {
-        namePicker.value = aValue;
-        // clear undo stack
-        namePicker.editor.transactionManager.clear();
-      }
-      break;
     case "uri":
-      var locationField = this._element("locationField");
-      if (locationField.value != aValue) {
-        this._uri = Cc["@mozilla.org/network/io-service;1"].
-                    getService(Ci.nsIIOService).
-                    newURI(aValue, null, null);
-        this._initTextField("locationField", this._uri.spec);
-        this._initNamePicker();
-        this._initTextField("tagsField",
-                             PlacesUtils.tagging
-                                        .getTagsForURI(this._uri).join(", "),
-                            false);
-        this._rebuildTagsSelectorList();
+      let newURI = NetUtil.newURI(aValue);
+      if (!newURI.equals(this._paneInfo.uri)) {
+        this._paneInfo.uri = newURI;
+        if (this._paneInfo.visibleRows.has("locationRow"))
+          this._initLocationField();
+
+        if (this._paneInfo.visibleRows.has("tagsRow")) {
+          delete this._paneInfo._cachedCommonTags;
+          this._onTagsChange(aItemId);
+        }
       }
       break;
     case "keyword":
-      this._initTextField("keywordField",
-                          PlacesUtils.bookmarks
-                                     .getKeywordForBookmark(this._itemId));
+      if (this._paneInfo.visibleRows.has("keywordRow"))
+        this._initKeywordField(aValue);
       break;
     case PlacesUIUtils.DESCRIPTION_ANNO:
-      this._initTextField("descriptionField",
-                          PlacesUIUtils.getItemDescription(this._itemId));
+      if (this._paneInfo.visibleRows.has("descriptionRow"))
+        this._initDescriptionField();
       break;
     case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO:
-      this._element("loadInSidebarCheckbox").checked =
-        PlacesUtils.annotations.itemHasAnnotation(this._itemId,
-                                                  PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
-      break;
-    case PlacesUtils.LMANNO_FEEDURI:
-      let feedURISpec =
-        PlacesUtils.annotations.getItemAnnotation(this._itemId,
-                                                  PlacesUtils.LMANNO_FEEDURI);
-      this._initTextField("feedLocationField", feedURISpec, true);
-      break;
-    case PlacesUtils.LMANNO_SITEURI:
-      let siteURISpec = "";
-      try {
-        siteURISpec =
-          PlacesUtils.annotations.getItemAnnotation(this._itemId,
-                                                    PlacesUtils.LMANNO_SITEURI);
-      } catch (ex) {}
-      this._initTextField("siteLocationField", siteURISpec, true);
+      if (this._paneInfo.visibleRows.has("loadInSidebarCheckbox"))
+        this._initLoadInSidebar();
       break;
     }
   },
 
-  onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex,
-                                        aNewParent, aNewIndex, aItemType) {
-    if (aItemId != this._itemId ||
-        aNewParent == this._getFolderIdFromMenuList())
+  onItemMoved(aItemId, aOldParent, aOldIndex,
+              aNewParent, aNewIndex, aItemType) {
+    if (!this._paneInfo.isItem ||
+        !this._paneInfo.visibleRows.has("folderPicker") ||
+        this._paneInfo.itemId != aItemOd ||
+        aNewParent == this._getFolderIdFromMenuList()) {
       return;
+    }
 
-    var folderItem = this._getFolderMenuItem(aNewParent);
-
-    // just setting selectItem _does not_ trigger oncommand, so we don't
-    // recurse
-    this._folderMenuList.selectedItem = folderItem;
+    // Just setting selectItem _does not_ trigger oncommand, so we don't
+    // recurse.
+    this._folderMenuList.selectedItem = this._getFolderMenuItem(aNewParent);
   },
 
-  onItemAdded: function EIO_onItemAdded(aItemId, aParentId, aIndex, aItemType,
-                                        aURI) {
+  onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI) {
     this._lastNewItem = aItemId;
   },
 
-  onItemRemoved: function() { },
-  onBeginUpdateBatch: function() { },
-  onEndUpdateBatch: function() { },
-  onItemVisited: function() { },
+  onItemRemoved() { },
+  onBeginUpdateBatch() { },
+  onEndUpdateBatch() { },
+  onItemVisited() { },
 };
+
+
+for (let elt of ["folderMenuList", "folderTree", "namePicker",
+                 "locationField", "descriptionField", "keywordField",
+                 "tagsField", "loadInSidebarCheckbox"]) {
+  let eltScoped = elt;
+  XPCOMUtils.defineLazyGetter(gEditItemOverlay, `_${eltScoped}`,
+                              () => gEditItemOverlay._element(eltScoped));
+}
--- a/browser/components/places/content/editBookmarkOverlay.xul
+++ b/browser/components/places/content/editBookmarkOverlay.xul
@@ -24,44 +24,28 @@
         <column flex="1" id="editBMPanel_editColumn" />
       </columns>
       <rows id="editBMPanel_rows">
         <row align="center" id="editBMPanel_nameRow">
           <label value="&editBookmarkOverlay.name.label;"
                  class="editBMPanel_rowLabel"
                  accesskey="&editBookmarkOverlay.name.accesskey;"
                  control="editBMPanel_namePicker"/>
-          <textbox id="editBMPanel_namePicker"/>
+          <textbox id="editBMPanel_namePicker"
+                   onchange="gEditItemOverlay.onNamePickerChange();"/>
         </row>
 
         <row align="center" id="editBMPanel_locationRow">
           <label value="&editBookmarkOverlay.location.label;"
                  class="editBMPanel_rowLabel"
                  accesskey="&editBookmarkOverlay.location.accesskey;"
                  control="editBMPanel_locationField"/>
           <textbox id="editBMPanel_locationField"
-                   class="uri-element"/>
-        </row>
-
-        <row align="center" id="editBMPanel_feedLocationRow">
-          <label value="&editBookmarkOverlay.feedLocation.label;"
-                 class="editBMPanel_rowLabel"
-                 accesskey="&editBookmarkOverlay.feedLocation.accesskey;"
-                 control="editBMPanel_feedLocationField"/>
-          <textbox id="editBMPanel_feedLocationField"
-                   class="uri-element"/>
-        </row>
-
-        <row align="center" id="editBMPanel_siteLocationRow">
-          <label value="&editBookmarkOverlay.siteLocation.label;"
-                 class="editBMPanel_rowLabel"
-                 accesskey="&editBookmarkOverlay.siteLocation.accesskey;"
-                 control="editBMPanel_siteLocationField"/>
-          <textbox id="editBMPanel_siteLocationField"
-                   class="uri-element"/>
+                   class="uri-element"
+                   onchange="gEditItemOverlay.onLocationFieldChange();"/>
         </row>
 
         <row align="center" id="editBMPanel_folderRow">
           <label value="&editBookmarkOverlay.folder.label;"
                  class="editBMPanel_rowLabel"
                  control="editBMPanel_folderMenuList"/>
           <hbox flex="1" align="center">
             <menulist id="editBMPanel_folderMenuList"
@@ -109,17 +93,17 @@
               </treecols>
               <treechildren flex="1"/>
             </tree>
 
             <hbox id="editBMPanel_newFolderBox">
               <button label="&editBookmarkOverlay.newFolderButton.label;"
                       id="editBMPanel_newFolderButton"
                       accesskey="&editBookmarkOverlay.newFolderButton.accesskey;"
-                      oncommand="gEditItemOverlay.newFolder();"/>
+                      oncommand="gEditItemOverlay.newFolder().catch(Components.utils.reportError);"/>
             </hbox>
           </vbox>
         </row>
 
         <row align="center" id="editBMPanel_tagsRow">
           <label value="&editBookmarkOverlay.tags.label;"
                  class="editBMPanel_rowLabel"
                  accesskey="&editBookmarkOverlay.tags.accesskey;"
@@ -128,17 +112,18 @@
             <textbox id="editBMPanel_tagsField"
                      type="autocomplete"
                      class="padded"
                      flex="1"
                      autocompletesearch="places-tag-autocomplete" 
                      completedefaultindex="true"
                      tabscrolling="true"
                      showcommentcolumn="true"
-                     placeholder="&editBookmarkOverlay.tagsEmptyDesc.label;"/>
+                     placeholder="&editBookmarkOverlay.tagsEmptyDesc.label;"
+                     onchange="gEditItemOverlay.onTagsFieldChange();"/>
             <button id="editBMPanel_tagsSelectorExpander"
                     class="expander-down"
                     tooltiptext="&editBookmarkOverlay.tagsExpanderDown.tooltip;"
                     tooltiptextdown="&editBookmarkOverlay.tagsExpanderDown.tooltip;"
                     tooltiptextup="&editBookmarkOverlay.expanderUp.tooltip;"
                     oncommand="gEditItemOverlay.toggleTagsSelector();"/>
           </hbox>
         </row>
@@ -152,28 +137,30 @@
         </row>
 
         <row align="center" id="editBMPanel_keywordRow">
           <observes element="additionalInfoBroadcaster" attribute="hidden"/>
           <label value="&editBookmarkOverlay.keyword.label;"
                  class="editBMPanel_rowLabel"
                  accesskey="&editBookmarkOverlay.keyword.accesskey;"
                  control="editBMPanel_keywordField"/>
-          <textbox id="editBMPanel_keywordField"/>
+          <textbox id="editBMPanel_keywordField"
+                   onchange="gEditItemOverlay.onKeywordFieldChange();"/>
         </row>
 
         <row id="editBMPanel_descriptionRow">
           <observes element="additionalInfoBroadcaster" attribute="hidden"/>
           <label value="&editBookmarkOverlay.description.label;"
                  class="editBMPanel_rowLabel"
                  accesskey="&editBookmarkOverlay.description.accesskey;"
                  control="editBMPanel_descriptionField"/>
           <textbox id="editBMPanel_descriptionField"
                    multiline="true"
-                   rows="4"/>
+                   rows="4"
+                   onchange="gEditItemOverlay.onDescriptionFieldChange();"/>
         </row>
       </rows>
     </grid>
 
     <checkbox id="editBMPanel_loadInSidebarCheckbox"
               label="&editBookmarkOverlay.loadInSidebar.label;"
               accesskey="&editBookmarkOverlay.loadInSidebar.accesskey;"
               oncommand="gEditItemOverlay.onLoadInSidebarCheckboxCommand();">
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -610,107 +610,92 @@ var PlacesOrganizer = {
 
   _fillDetailsPane: function PO__fillDetailsPane(aNodeList) {
     var infoBox = document.getElementById("infoBox");
     var detailsDeck = document.getElementById("detailsDeck");
 
     // Make sure the infoBox UI is visible if we need to use it, we hide it
     // below when we don't.
     infoBox.hidden = false;
-    var aSelectedNode = aNodeList.length == 1 ? aNodeList[0] : null;
+    let selectedNode = aNodeList.length == 1 ? aNodeList[0] : null;
+
     // If a textbox within a panel is focused, force-blur it so its contents
     // are saved
     if (gEditItemOverlay.itemId != -1) {
       var focusedElement = document.commandDispatcher.focusedElement;
       if ((focusedElement instanceof HTMLInputElement ||
            focusedElement instanceof HTMLTextAreaElement) &&
           /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id))
         focusedElement.blur();
 
       // don't update the panel if we are already editing this node unless we're
       // in multi-edit mode
-      if (aSelectedNode) {
-        var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
-        var nodeIsSame = gEditItemOverlay.itemId == aSelectedNode.itemId ||
+      if (selectedNode) {
+        var concreteId = PlacesUtils.getConcreteItemId(selectedNode);
+        var nodeIsSame = gEditItemOverlay.itemId == selectedNode.itemId ||
                          gEditItemOverlay.itemId == concreteId ||
-                         (aSelectedNode.itemId == -1 && gEditItemOverlay.uri &&
-                          gEditItemOverlay.uri == aSelectedNode.uri);
+                         (selectedNode.itemId == -1 && gEditItemOverlay.uri &&
+                          gEditItemOverlay.uri == selectedNode.uri);
         if (nodeIsSame && detailsDeck.selectedIndex == 1 &&
             !gEditItemOverlay.multiEdit)
           return;
       }
     }
 
     // Clean up the panel before initing it again.
     gEditItemOverlay.uninitPanel(false);
 
-    if (aSelectedNode && !PlacesUtils.nodeIsSeparator(aSelectedNode)) {
+    if (selectedNode && !PlacesUtils.nodeIsSeparator(selectedNode)) {
       detailsDeck.selectedIndex = 1;
       // Using the concrete itemId is arguably wrong.  The bookmarks API
       // does allow setting properties for folder shortcuts as well, but since
       // the UI does not distinct between the couple, we better just show
       // the concrete item properties for shortcuts to root nodes.
-      var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
+      var concreteId = PlacesUtils.getConcreteItemId(selectedNode);
       var isRootItem = concreteId != -1 && PlacesUtils.isRootItem(concreteId);
       var readOnly = isRootItem ||
-                     aSelectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId;
+                     selectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId;
       var useConcreteId = isRootItem ||
-                          PlacesUtils.nodeIsTagQuery(aSelectedNode);
+                          PlacesUtils.nodeIsTagQuery(selectedNode);
       var itemId = -1;
       if (concreteId != -1 && useConcreteId)
         itemId = concreteId;
-      else if (aSelectedNode.itemId != -1)
-        itemId = aSelectedNode.itemId;
+      else if (selectedNode.itemId != -1)
+        itemId = selectedNode.itemId;
       else
-        itemId = PlacesUtils._uri(aSelectedNode.uri);
-
-      gEditItemOverlay.initPanel(itemId, { hiddenRows: ["folderPicker"]
-                                         , forceReadOnly: readOnly
-                                         , titleOverride: aSelectedNode.title
-                                         });
+        itemId = PlacesUtils._uri(selectedNode.uri);
 
-      // Dynamically generated queries, like history date containers, have
-      // itemId !=0 and do not exist in history.  For them the panel is
-      // read-only, but empty, since it can't get a valid title for the object.
-      // In such a case we force the title using the selectedNode one, for UI
-      // polishness.
-      if (aSelectedNode.itemId == -1 &&
-          (PlacesUtils.nodeIsDay(aSelectedNode) ||
-           PlacesUtils.nodeIsHost(aSelectedNode)))
-        gEditItemOverlay._element("namePicker").value = aSelectedNode.title;
+      gEditItemOverlay.initPanel({ node: selectedNode
+                                 , hiddenRows: ["folderPicker"] });
 
-      this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
+      this._detectAndSetDetailsPaneMinimalState(selectedNode);
     }
-    else if (!aSelectedNode && aNodeList[0]) {
-      var itemIds = [];
-      for (var i = 0; i < aNodeList.length; i++) {
-        if (!PlacesUtils.nodeIsBookmark(aNodeList[i]) &&
-            !PlacesUtils.nodeIsURI(aNodeList[i])) {
-          detailsDeck.selectedIndex = 0;
-          var selectItemDesc = document.getElementById("selectItemDescription");
-          var itemsCountLabel = document.getElementById("itemsCountText");
-          selectItemDesc.hidden = false;
-          itemsCountLabel.value =
-            PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
-                                          aNodeList.length, [aNodeList.length]);
-          infoBox.hidden = true;
-          return;
-        }
-        itemIds[i] = aNodeList[i].itemId != -1 ? aNodeList[i].itemId :
-                     PlacesUtils._uri(aNodeList[i].uri);
+    else if (!selectedNode && aNodeList[0]) {
+      if (aNodeList.every(PlacesUtils.nodeIsURI)) {
+        let uris = [for (node of aNodeList) PlacesUtils._uri(node.uri)];
+        detailsDeck.selectedIndex = 1;
+        gEditItemOverlay.initPanel({ uris
+                                   , hiddenRows: ["folderPicker",
+                                                  "loadInSidebar",
+                                                  "location",
+                                                  "keyword",
+                                                  "description",
+                                                  "name"]});
+        this._detectAndSetDetailsPaneMinimalState(selectedNode);
       }
-      detailsDeck.selectedIndex = 1;
-      gEditItemOverlay.initPanel(itemIds,
-                                 { hiddenRows: ["folderPicker",
-                                                "loadInSidebar",
-                                                "location",
-                                                "keyword",
-                                                "description",
-                                                "name"]});
-      this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
+      else {
+        detailsDeck.selectedIndex = 0;
+        let selectItemDesc = document.getElementById("selectItemDescription");
+        let itemsCountLabel = document.getElementById("itemsCountText");
+        selectItemDesc.hidden = false;
+        itemsCountLabel.value =
+          PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
+                                        aNodeList.length, [aNodeList.length]);
+        infoBox.hidden = true;
+      }
     }
     else {
       detailsDeck.selectedIndex = 0;
       infoBox.hidden = true;
       let selectItemDesc = document.getElementById("selectItemDescription");
       let itemsCountLabel = document.getElementById("itemsCountText");
       let itemsCount = 0;
       if (ContentArea.currentView.result) {
--- a/browser/components/places/content/treeView.js
+++ b/browser/components/places/content/treeView.js
@@ -1686,18 +1686,23 @@ PlacesTreeView.prototype = {
 
     return true;
   },
 
   setCellText: function PTV_setCellText(aRow, aColumn, aText) {
     // We may only get here if the cell is editable.
     let node = this._rows[aRow];
     if (node.title != aText) {
-      let txn = new PlacesEditItemTitleTransaction(node.itemId, aText);
-      PlacesUtils.transactionManager.doTransaction(txn);
+      if (!PlacesUIUtils.useAsyncTransactions) {
+        let txn = new PlacesEditItemTitleTransaction(node.itemId, aText);
+        PlacesUtils.transactionManager.doTransaction(txn);
+        return;
+      }
+      PlacesTransactions.EditTitle({ guid: node.bookmarkGuid, title: aText })
+                        .transact().catch(Cu.reportError);
     }
   },
 
   toggleCutNode: function PTV_toggleCutNode(aNode, aValue) {
     let currentVal = this._cuttingNodes.has(aNode);
     if (currentVal != aValue) {
       if (aValue)
         this._cuttingNodes.add(aNode);
--- a/browser/components/places/tests/browser/browser_bookmarksProperties.js
+++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js
@@ -108,30 +108,29 @@ gTests.push({
     // Select Unfiled Bookmarks root.
     var itemId = PlacesUIUtils.leftPaneQueries["UnfiledBookmarks"];
     tree.selectItems([itemId]);
     this.selectedNode = tree.selectedNode;
   },
 
   run: function() {
     // Check that the dialog is read-only.
-    ok(this.window.BookmarkPropertiesPanel._readOnly, "Dialog is read-only");
-
+    ok(this.window.gEditItemOverlay.readOnly, "Dialog is read-only");
     // Check that accept button is disabled
     var acceptButton = this.window.document.documentElement.getButton("accept");
     ok(acceptButton.disabled, "Accept button is disabled");
 
     // Check that name picker is read only
     var namepicker = this.window.document.getElementById("editBMPanel_namePicker");
     ok(namepicker.readOnly, "Name field is disabled");
     is(namepicker.value,
        PlacesUtils.bookmarks.getItemTitle(PlacesUtils.unfiledBookmarksFolderId),
        "Node title is correct");
     // Blur the field and ensure root's name has not been changed.
-    this.window.gEditItemOverlay.onNamePickerBlur();
+    this.window.gEditItemOverlay._namePicker.blur();
     is(namepicker.value,
        PlacesUtils.bookmarks.getItemTitle(PlacesUtils.unfiledBookmarksFolderId),
        "Root title is correct");
     // Check the shortcut's title.
     is(PlacesUtils.bookmarks.getItemTitle(this.selectedNode.itemId), null,
        "Shortcut title is null");
     this.finish();
   },
@@ -144,17 +143,16 @@ gTests.push({
 
   cleanup: function() {
     // Nothing to do.
   }
 });
 
 //------------------------------------------------------------------------------
 // Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog
-
 gTests.push({
   desc: "Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog",
   sidebar: SIDEBAR_BOOKMARKS_ID,
   action: ACTION_EDIT,
   itemType: null,
   window: null,
   _itemId: null,
   _cleanShutdown: false,
@@ -221,19 +219,21 @@ gTests.push({
         }
       }
     };
     tagsField.popup.addEventListener("popupshown", popupListener, true);
     tagsField.popup.addEventListener("popuphidden", popupListener, true);
 
     // Open tags autocomplete popup.
     info("About to focus the tagsField");
-    tagsField.focus();
-    tagsField.value = "";
-    EventUtils.synthesizeKey("t", {}, this.window);
+    executeSoon(() => {
+                  tagsField.focus();
+                  tagsField.value = "";
+                  EventUtils.synthesizeKey("t", {}, this.window);
+                });
   },
 
   finish: function() {
     SidebarUI.hide();
     runNextTest();
   },
 
   cleanup: function() {
@@ -242,19 +242,20 @@ gTests.push({
     is(tags[0], "testTag", "Tag on node has not changed");
 
     // Cleanup.
     PlacesUtils.tagging.untagURI(PlacesUtils._uri(TEST_URL), ["testTag"]);
     PlacesUtils.bookmarks.removeItem(this._itemId);
   }
 });
 
+
 //------------------------------------------------------------------------------
 // Bug 475529 -  Add button in new folder dialog not default anymore
-
+/*
 gTests.push({
   desc: "Bug 475529 - Add button in new folder dialog not default anymore",
   sidebar: SIDEBAR_BOOKMARKS_ID,
   action: ACTION_ADD,
   itemType: TYPE_FOLDER,
   window: null,
   _itemId: null,
 
@@ -266,31 +267,31 @@ gTests.push({
   selectNode: function(tree) {
     // Select Unfiled Bookmarks root.
     var itemId = PlacesUIUtils.leftPaneQueries["UnfiledBookmarks"];
     tree.selectItems([itemId]);
     this.selectedNode = tree.selectedNode;
   },
 
   run: function() {
-    this._itemId = this.window.gEditItemOverlay._itemId;
+    this._itemId = this.window.gEditItemOverlay._paneInfo.itemId;
     // Change folder name
     var namePicker = this.window.document.getElementById("editBMPanel_namePicker");
     var self = this;
 
     this.window.addEventListener("unload", function(event) {
       self.window.removeEventListener("unload", arguments.callee, false);
       executeSoon(function () {
         self.finish();
       });
     }, false);
 
-    namePicker.value = "n";
     info("About to focus the namePicker field");
     namePicker.focus();
+    EventUtils.synthesizeKey("n", {}, this.window);
     EventUtils.synthesizeKey("VK_RETURN", {}, this.window);
   },
 
   finish: function() {
     // Window is already closed.
     SidebarUI.hide();
     runNextTest();
   },
@@ -299,16 +300,17 @@ gTests.push({
     // Check that folder name has been changed.
     is(PlacesUtils.bookmarks.getItemTitle(this._itemId), "n",
        "Folder name has been edited");
 
     // Cleanup.
     PlacesUtils.bookmarks.removeItem(this._itemId);
   }
 });
+*/
 
 //------------------------------------------------------------------------------
 // Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel
 
 gTests.push({
   desc: "Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel",
   sidebar: SIDEBAR_BOOKMARKS_ID,
   action: ACTION_EDIT,
@@ -557,17 +559,17 @@ function open_properties_dialog() {
         return;
       ww.unregisterNotification(windowObserver);
       var win = aSubject.QueryInterface(Ci.nsIDOMWindow);
       win.addEventListener("focus", function (event) {
         win.removeEventListener("focus", arguments.callee, false);
         // Windows has been loaded, execute our test now.
         executeSoon(function () {
           // Ensure overlay is loaded
-          ok(win.gEditItemOverlay._initialized, "EditItemOverlay is initialized");
+          ok(win.gEditItemOverlay.initialized, "EditItemOverlay is initialized");
           gCurrentTest.window = win;
           try {
             gCurrentTest.run();
           } catch (ex) {
             ok(false, "An error occured during test run: " + ex.message);
           }
         });
       }, false);
--- a/browser/components/places/tests/browser/browser_library_panel_leak.js
+++ b/browser/components/places/tests/browser/browser_library_panel_leak.js
@@ -20,17 +20,17 @@ const TEST_URI = "http://www.mozilla.org
 
 function test() {
   function onLibraryReady(organizer) {
     let contentTree = organizer.document.getElementById("placeContent");
     isnot(contentTree, null, "Sanity check: placeContent tree should exist");
     isnot(organizer.PlacesOrganizer, null, "Sanity check: PlacesOrganizer should exist");
     isnot(organizer.gEditItemOverlay, null, "Sanity check: gEditItemOverlay should exist");
 
-    ok(organizer.gEditItemOverlay._initialized, "gEditItemOverlay is initialized");
+    ok(organizer.gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
     isnot(organizer.gEditItemOverlay.itemId, -1, "Editing a bookmark");
 
     // Select History in the left pane.
     organizer.PlacesOrganizer.selectLeftPaneQuery('History');
     // Select the first history entry.
     let selection = contentTree.view.selection;
     selection.clearSelection();
     selection.rangedSelect(0, 0, true);
--- a/browser/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul
+++ b/browser/components/places/tests/chrome/test_bug427633_no_newfolder_if_noip.xul
@@ -51,19 +51,19 @@
           url: "http://www.example.com/",
           index: PlacesUtils.bookmarks.DEFAULT_INDEX,
           type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
           title: "mozilla"
         });
 
         // Init panel.
         ok(gEditItemOverlay, "gEditItemOverlay is in context");
-        let itemId = yield PlacesUtils.promiseItemId(bm.guid);
-        gEditItemOverlay.initPanel(itemId);
-        ok(gEditItemOverlay._initialized, "gEditItemOverlay is initialized");
+        let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+        gEditItemOverlay.initPanel({ node });
+        ok(gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
 
         let tree = gEditItemOverlay._element("folderTree");
         yield openFolderTree(tree);
 
         tree.view.selection.clearSelection();
         ok(document.getElementById("editBMPanel_newFolderButton").disabled,
            "New folder button is disabled if there's no selection");
 
--- a/browser/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul
+++ b/browser/components/places/tests/chrome/test_bug485100-change-case-loses-tag.xul
@@ -50,29 +50,29 @@
           index: PlacesUtils.bookmarks.DEFAULT_INDEX,
           type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
           title: "mozilla",
           url: testURI
         });
 
         // Init panel
         ok(gEditItemOverlay, "gEditItemOverlay is in context");
-        let itemId = yield PlacesUtils.promiseItemId(bm.guid);
-        gEditItemOverlay.initPanel(itemId);
+        let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+        gEditItemOverlay.initPanel({ node });
 
         // add a tag
         document.getElementById("editBMPanel_tagsField").value = testTag;
-        gEditItemOverlay.onTagsFieldBlur();
+        gEditItemOverlay.onTagsFieldChange();
 
         // test that the tag has been added in the backend
         is(PlacesUtils.tagging.getTagsForURI(testURI)[0], testTag, "tags match");
 
         // change the tag
         document.getElementById("editBMPanel_tagsField").value = testTagUpper;
-        gEditItemOverlay.onTagsFieldBlur();
+        gEditItemOverlay.onTagsFieldChange();
 
         // test that the tag has been added in the backend
         is(PlacesUtils.tagging.getTagsForURI(testURI)[0], testTagUpper, "tags match");
 
         // Cleanup.
         PlacesUtils.tagging.untagURI(testURI, [testTag]);
         yield PlacesUtils.bookmarks.remove(bm.guid);
       }).then(() => SimpleTest.finish());
--- a/browser/components/places/tests/chrome/test_bug631374_tags_selector_scroll.xul
+++ b/browser/components/places/tests/chrome/test_bug631374_tags_selector_scroll.xul
@@ -68,19 +68,19 @@
           type: bs.TYPE_BOOKMARK,
           title: "mozilla",
           url: uri2.spec
         });
         PlacesUtils.tagging.tagURI(uri2, tags);
 
         // Init panel.
         ok(gEditItemOverlay, "gEditItemOverlay is in context");
-        let id1 = yield PlacesUtils.promiseItemId(bm1.guid);
-        gEditItemOverlay.initPanel(id1);
-        ok(gEditItemOverlay._initialized, "gEditItemOverlay is initialized");
+        let node1 = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm1);
+        gEditItemOverlay.initPanel({ node: node1 });
+        ok(gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
 
         yield openTagSelector();
         let tagsSelector = document.getElementById("editBMPanel_tagsSelector");
 
         // Go by two so there is some untouched tag in the middle.
         for (let i = 8; i < tags.length; i += 2) {
           tagsSelector.selectedIndex = i;
           let listItem = tagsSelector.selectedItem;
--- a/browser/components/places/tests/chrome/test_editBookmarkOverlay_tags_liveUpdate.xul
+++ b/browser/components/places/tests/chrome/test_editBookmarkOverlay_tags_liveUpdate.xul
@@ -70,18 +70,18 @@
           parentGuid: PlacesUtils.bookmarks.unfiledGuid,
           index: PlacesUtils.bookmarks.DEFAULT_INDEX,
           type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
           url: TEST_URI.spec,
           title: "test.me"
         });
 
         // Init panel.
-        itemId = yield PlacesUtils.promiseItemId(bm.guid);
-        gEditItemOverlay.initPanel(itemId);
+        let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
+        gEditItemOverlay.initPanel({ node });
 
         // Add a tag.
         PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
 
         is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
            "Correctly added tag to a single bookmark");
         is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
            "Editing a single bookmark shows the added tag");
@@ -99,29 +99,28 @@
         let bm2 = yield PlacesUtils.bookmarks.insert({
           parentGuid: PlacesUtils.bookmarks.unfiledGuid,
           index: PlacesUtils.bookmarks.DEFAULT_INDEX,
           type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
           title: "test.again.me",
           url: TEST_URI2.spec
         });
 
-        // Init panel with multiple bookmarks.
-        itemId2 = yield PlacesUtils.promiseItemId(bm2.guid);
-        gEditItemOverlay.initPanel([itemId, itemId2]);
+        // Init panel with multiple uris.
+        gEditItemOverlay.initPanel({ uris: [TEST_URI, TEST_URI2] });
 
-        // Add a tag to the first bookmark.
+        // Add a tag to the first uri.
         PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
         is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
            "Correctly added a tag to the first bookmark.");
         is(document.getElementById("editBMPanel_tagsField").value, "",
            "Editing multiple bookmarks without matching tags should not show any tag.");
         checkTagsSelector([TEST_TAG], []);
 
-        // Add a tag to the second bookmark.
+        // Add a tag to the second uri.
         PlacesUtils.tagging.tagURI(TEST_URI2, [TEST_TAG]);
         is(PlacesUtils.tagging.getTagsForURI(TEST_URI2)[0], TEST_TAG,
            "Correctly added a tag to the second bookmark.");
         is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
            "Editing multiple bookmarks should show matching tags.");
         checkTagsSelector([TEST_TAG], [TEST_TAG]);
 
         // Remove tag from the first bookmark.
@@ -136,17 +135,17 @@
         PlacesUtils.tagging.untagURI(TEST_URI2, [TEST_TAG]);
         is(PlacesUtils.tagging.getTagsForURI(TEST_URI2)[0], undefined,
            "Correctly removed tag from the second bookmark.");
         is(document.getElementById("editBMPanel_tagsField").value, "",
            "Editing multiple bookmarks without matching tags should not show any tag.");
         checkTagsSelector([], []);
 
         // Init panel with a nsIURI entry.
-        gEditItemOverlay.initPanel(TEST_URI);
+        gEditItemOverlay.initPanel({ uris: [TEST_URI] });
 
         // Add a tag.
         PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
         is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
            "Correctly added tag to the first entry.");
         is(document.getElementById("editBMPanel_tagsField").value, TEST_TAG,
            "Editing a single nsIURI entry shows the added tag");
         checkTagsSelector([TEST_TAG], [TEST_TAG]);
@@ -155,17 +154,17 @@
         PlacesUtils.tagging.untagURI(TEST_URI, [TEST_TAG]);
         is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], undefined,
            "Correctly removed tag from the nsIURI entry.");
         is(document.getElementById("editBMPanel_tagsField").value, "",
            "Editing a single nsIURI entry should not show any tag");
         checkTagsSelector([], []);
 
         // Init panel with multiple nsIURI entries.
-        gEditItemOverlay.initPanel([TEST_URI, TEST_URI2]);
+        gEditItemOverlay.initPanel({ uris: [TEST_URI, TEST_URI2] });
 
         // Add a tag to the first entry.
         PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
         is(PlacesUtils.tagging.getTagsForURI(TEST_URI)[0], TEST_TAG,
            "Tag correctly added.");
         is(document.getElementById("editBMPanel_tagsField").value, "",
            "Editing multiple nsIURIs without matching tags should not show any tag.");
         checkTagsSelector([TEST_TAG], []);
--- a/browser/components/sessionstore/SessionFile.jsm
+++ b/browser/components/sessionstore/SessionFile.jsm
@@ -50,16 +50,19 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyServiceGetter(this, "sessionStartup",
   "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionWorker",
   "resource:///modules/sessionstore/SessionWorker.jsm");
 
 const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
 const PREF_MAX_UPGRADE_BACKUPS = "browser.sessionstore.upgradeBackup.maxUpgradeBackups";
 
+const PREF_MAX_SERIALIZE_BACK = "browser.sessionstore.max_serialize_back";
+const PREF_MAX_SERIALIZE_FWD = "browser.sessionstore.max_serialize_forward";
+
 this.SessionFile = {
   /**
    * Read the contents of the session file, asynchronously.
    */
   read: function () {
     return SessionFileInternal.read();
   },
   /**
@@ -251,21 +254,21 @@ let SessionFileInternal = {
         parsed: null
       };
     }
 
     result.noFilesFound = noFilesFound;
 
     // Initialize the worker to let it handle backups and also
     // as a workaround for bug 964531.
-    SessionWorker.post("init", [
-      result.origin,
-      this.Paths,
-      Preferences.get(PREF_MAX_UPGRADE_BACKUPS, 3)
-    ]);
+    SessionWorker.post("init", [result.origin, this.Paths, {
+      maxUpgradeBackups: Preferences.get(PREF_MAX_UPGRADE_BACKUPS, 3),
+      maxSerializeBack: Preferences.get(PREF_MAX_SERIALIZE_BACK, 10),
+      maxSerializeForward: Preferences.get(PREF_MAX_SERIALIZE_FWD, -1)
+    }]);
 
     return result;
   }),
 
   gatherTelemetry: function(aStateString) {
     return Task.spawn(function() {
       let msg = yield SessionWorker.post("gatherTelemetry", [aStateString]);
       this._recordTelemetry(msg.telemetry);
--- a/browser/components/sessionstore/SessionHistory.jsm
+++ b/browser/components/sessionstore/SessionHistory.jsm
@@ -65,63 +65,26 @@ let SessionHistoryInternal = {
    */
   collect: function (docShell) {
     let data = {entries: []};
     let isPinned = docShell.isAppTab;
     let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
     let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
 
     if (history && history.count > 0) {
-      let oldest;
-      let maxSerializeBack =
-        Services.prefs.getIntPref("browser.sessionstore.max_serialize_back");
-      if (maxSerializeBack >= 0) {
-        oldest = Math.max(0, history.index - maxSerializeBack);
-      } else { // History.getEntryAtIndex(0, ...) is the oldest.
-        oldest = 0;
-      }
-
-      let newest;
-      let maxSerializeFwd =
-        Services.prefs.getIntPref("browser.sessionstore.max_serialize_forward");
-      if (maxSerializeFwd >= 0) {
-        newest = Math.min(history.count - 1, history.index + maxSerializeFwd);
-      } else { // History.getEntryAtIndex(history.count - 1, ...) is the newest.
-        newest = history.count - 1;
-      }
-
       // Loop over the transaction linked list directly so we can get the
       // persist property for each transaction.
-      let txn = history.rootTransaction;
-      let i = 0;
-      while (txn && i < oldest) {
-        txn = txn.next;
-        i++;
-      }
-
-      while (txn && i <= newest) {
-        let shEntry = txn.sHEntry;
-        let entry = this.serializeEntry(shEntry, isPinned);
+      for (let txn = history.rootTransaction; txn; txn = txn.next) {
+        let entry = this.serializeEntry(txn.sHEntry, isPinned);
         entry.persist = txn.persist;
         data.entries.push(entry);
-        txn = txn.next;
-        i++;
       }
 
-      if (i <= newest) {
-        // In some cases, there don't seem to be as many history entries as
-        // history.count claims. we'll save whatever history we can, print an
-        // error message, and still save sessionstore.js.
-        debug("SessionStore failed gathering complete history " +
-              "for the focused window/tab. See bug 669196.");
-      }
-
-      // Set the one-based index of the currently active tab,
-      // ensuring it isn't out of bounds if an exception was thrown above.
-      data.index = Math.min(history.index - oldest + 1, data.entries.length);
+      // Ensure the index isn't out of bounds if an exception was thrown above.
+      data.index = Math.min(history.index + 1, data.entries.length);
     }
 
     // If either the session history isn't available yet or doesn't have any
     // valid entries, make sure we at least include the current page.
     if (data.entries.length == 0) {
       let uri = webNavigation.currentURI.spec;
       let body = webNavigation.document.body;
       // We landed here because the history is inaccessible or there are no
--- a/browser/components/sessionstore/SessionSaver.jsm
+++ b/browser/components/sessionstore/SessionSaver.jsm
@@ -241,36 +241,30 @@ let SessionSaverInternal = {
     // Write to disk.
     this._saveState();
   },
 
   /**
    * Write the given state object to disk.
    */
   _writeState: function (state) {
-    // Inform observers
-    notify(null, "sessionstore-state-write");
-
-    stopWatchStart("SERIALIZE_DATA_MS", "SERIALIZE_DATA_LONGEST_OP_MS", "WRITE_STATE_LONGEST_OP_MS");
-    let data = JSON.stringify(state);
-    stopWatchFinish("SERIALIZE_DATA_MS", "SERIALIZE_DATA_LONGEST_OP_MS");
+    stopWatchStart("WRITE_STATE_LONGEST_OP_MS");
 
     // We update the time stamp before writing so that we don't write again
     // too soon, if saving is requested before the write completes. Without
     // this update we may save repeatedly if actions cause a runDelayed
     // before writing has completed. See Bug 902280
     this.updateLastSaveTime();
 
     // Write (atomically) to a session file, using a tmp file. Once the session
     // file is successfully updated, save the time stamp of the last save and
     // notify the observers.
-    stopWatchStart("SEND_SERIALIZED_STATE_LONGEST_OP_MS");
-    let promise = SessionFile.write(data);
-    stopWatchFinish("WRITE_STATE_LONGEST_OP_MS",
-                    "SEND_SERIALIZED_STATE_LONGEST_OP_MS");
+    let promise = SessionFile.write(state);
+    stopWatchFinish("WRITE_STATE_LONGEST_OP_MS");
+
     promise = promise.then(() => {
       this.updateLastSaveTime();
       notify(null, "sessionstore-state-write-complete");
     }, console.error);
 
     return promise;
   },
 };
--- a/browser/components/sessionstore/SessionWorker.js
+++ b/browser/components/sessionstore/SessionWorker.js
@@ -83,46 +83,77 @@ let Agent = {
   maxUpgradeBackups: null,
 
   /**
    * Initialize (or reinitialize) the worker
    *
    * @param {string} origin Which of sessionstore.js or its backups
    *   was used. One of the `STATE_*` constants defined above.
    * @param {object} paths The paths at which to find the various files.
-   * @param {number} maxUpgradeBackups The number of old upgrade backups that should be kept.
+   * @param {object} prefs The preferences the worker needs to known.
    */
-  init: function (origin, paths, maxUpgradeBackups) {
+  init(origin, paths, prefs = {}) {
     if (!(origin in paths || origin == STATE_EMPTY)) {
       throw new TypeError("Invalid origin: " + origin);
     }
+
+    // Check that all required preference values were passed.
+    for (let pref of ["maxUpgradeBackups", "maxSerializeBack", "maxSerializeForward"]) {
+      if (!prefs.hasOwnProperty(pref)) {
+        throw new TypeError(`Missing preference value for ${pref}`);
+      }
+    }
+
     this.state = origin;
     this.Paths = paths;
-    this.maxUpgradeBackups = maxUpgradeBackups || 3;
+    this.maxUpgradeBackups = prefs.maxUpgradeBackups;
+    this.maxSerializeBack = prefs.maxSerializeBack;
+    this.maxSerializeForward = prefs.maxSerializeForward;
     this.upgradeBackupNeeded = paths.nextUpgradeBackup != paths.upgradeBackup;
     return {result: true};
   },
 
   /**
    * Write the session to disk.
    * Write the session to disk, performing any necessary backup
    * along the way.
    *
-   * @param {string} stateString The state to write to disk.
+   * @param {object} state The state to write to disk.
    * @param {object} options
    *  - performShutdownCleanup If |true|, we should
    *    perform shutdown-time cleanup to ensure that private data
    *    is not left lying around;
    *  - isFinalWrite If |true|, write to Paths.clean instead of
    *    Paths.recovery
    */
-  write: function (stateString, options = {}) {
+  write: function (state, options = {}) {
     let exn;
     let telemetry = {};
 
+    // Cap the number of backward and forward shistory entries on shutdown.
+    if (options.isFinalWrite) {
+      for (let window of state.windows) {
+        for (let tab of window.tabs) {
+          let lower = 0;
+          let upper = tab.entries.length;
+
+          if (this.maxSerializeBack > -1) {
+            lower = Math.max(lower, tab.index - this.maxSerializeBack - 1);
+          }
+          if (this.maxSerializeForward > -1) {
+            upper = Math.min(upper, tab.index + this.maxSerializeForward);
+          }
+
+          tab.entries = tab.entries.slice(lower, upper);
+          tab.index -= lower;
+        }
+      }
+    }
+
+    let stateString = JSON.stringify(state);
     let data = Encoder.encode(stateString);
     let startWriteMs, stopWriteMs;
 
     try {
 
       if (this.state == STATE_CLEAN || this.state == STATE_EMPTY) {
         // The backups directory may not exist yet. In all other cases,
         // we have either already read from or already written to this
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -78,17 +78,16 @@ skip-if = !e10s || !crashreporter
 [browser_form_restore_events.js]
 [browser_formdata.js]
 skip-if = buildapp == 'mulet'
 [browser_formdata_format.js]
 [browser_formdata_xpath.js]
 [browser_frametree.js]
 [browser_frame_history.js]
 [browser_global_store.js]
-[browser_history_cap.js]
 [browser_history_persist.js]
 [browser_label_and_icon.js]
 [browser_merge_closed_tabs.js]
 [browser_pageStyle.js]
 [browser_privatetabs.js]
 [browser_replace_load.js]
 [browser_restore_redirect.js]
 [browser_scrollPositions.js]
deleted file mode 100644
--- a/browser/components/sessionstore/test/browser_history_cap.js
+++ /dev/null
@@ -1,121 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-/**
- * This test ensures that the preferences (added in bug 943339) that control how
- * many back and forward button session history entries we store work correctly.
- *
- * It adds a number of entries to the session history, restores them and checks
- * that the restored state matches the preferences.
- */
-
-add_task(function *test_history_cap() {
-  const baseURL = "http://example.com/browser_history_cap#"
-  const maxEntries  = 9; // The number of generated session history entries.
-  const middleEntry = 4; // The zero-based index of the middle entry.
-
-  const maxBack1 = 2; // The history cap settings used for the first test,
-  const maxFwd1 = 3;  // where maxBack1 + 1 + maxFwd1 < maxEntries.
-
-  const maxBack2 = 5; // The history cap settings used for the other tests, 
-  const maxFwd2 = 5;  // where maxBack2 + 1 + maxFwd2 > maxEntries.
-
-  // Set the relevant preferences for the first test.
-  gPrefService.setIntPref("browser.sessionhistory.max_entries", maxEntries);
-  gPrefService.setIntPref("browser.sessionstore.max_serialize_back", maxBack1);
-  gPrefService.setIntPref("browser.sessionstore.max_serialize_forward", maxFwd1);
-
-  // Make sure the settings we modify are reset afterward.
-  registerCleanupFunction(() => {
-    gPrefService.clearUserPref("browser.sessionhistory.max_entries");
-    gPrefService.clearUserPref("browser.sessionstore.max_serialize_back");
-    gPrefService.clearUserPref("browser.sessionstore.max_serialize_forward");
-  });
-
-  let tab = gBrowser.addTab();
-  let browser = tab.linkedBrowser;
-  yield promiseBrowserLoaded(browser);
-
-  // Generate the tab state entries and set the one-based
-  // tab-state index to the middle session history entry.
-  let tabState = {entries: [], index: middleEntry + 1};
-  for (let i = 0; i < maxEntries; i++) {
-    tabState.entries.push({url: baseURL + i});
-  }
-
-  info("Testing situation where only a subset of session history entries should be restored.");
-
-  yield promiseTabState(tab, tabState);
-  TabState.flush(tab.linkedBrowser);
-
-  let restoredTabState = JSON.parse(ss.getTabState(tab));
-  is(restoredTabState.entries.length, maxBack1 + 1 + maxFwd1,
-    "The expected number of session history entries was restored.");
-  is(restoredTabState.index, maxBack1 + 1, "The restored tab-state index is correct");
-
-  let indexURLOffset = middleEntry - (restoredTabState.index - 1);
-  for (let i = 0; i < restoredTabState.entries.length; i++) {
-    is(restoredTabState.entries[i].url, baseURL + (i + indexURLOffset),
-        "URL of restored entry matches the expected URL.");
-  }
-
-  // Set the relevant preferences for the other tests.
-  gPrefService.setIntPref("browser.sessionstore.max_serialize_back", maxBack2);
-  gPrefService.setIntPref("browser.sessionstore.max_serialize_forward", maxFwd2);
-
-  info("Testing situation where all of the entries in the session history should be restored.");
-
-  yield promiseTabState(tab, tabState);
-  TabState.flush(tab.linkedBrowser);
-
-  restoredTabState = JSON.parse(ss.getTabState(tab));
-  is(restoredTabState.entries.length, maxEntries,
-    "The expected number of session history entries was restored.");
-  is(restoredTabState.index, middleEntry + 1, "The restored tab-state index is correct");
-
-  for (let i = middleEntry - 2; i <= middleEntry + 2; i++) {
-    is(restoredTabState.entries[i].url, baseURL + i,
-        "URL of restored entry matches the expected URL.");
-  }
-
-  info("Testing situation where only the 1 + maxFwd2 oldest entries should be restored.");
-
-  // Set the one-based tab-state index to the oldest session history entry.
-  tabState.index = 1;
-
-  yield promiseTabState(tab, tabState);
-  TabState.flush(tab.linkedBrowser);
-
-  restoredTabState = JSON.parse(ss.getTabState(tab));
-  is(restoredTabState.entries.length, 1 + maxFwd2,
-    "The expected number of session history entries was restored.");
-  is(restoredTabState.index, 1, "The restored tab-state index is correct");
-
-  for (let i = 0; i <= 2; i++) {
-    is(restoredTabState.entries[i].url, baseURL + i,
-        "URL of restored entry matches the expected URL.");
-  }
-
-  info("Testing situation where only the maxBack2 + 1 newest entries should be restored.");
-
-  // Set the one-based tab-state index to the newest session history entry.
-  tabState.index = maxEntries;
-
-  yield promiseTabState(tab, tabState);
-  TabState.flush(tab.linkedBrowser);
-
-  restoredTabState = JSON.parse(ss.getTabState(tab));
-  is(restoredTabState.entries.length, maxBack2 + 1,
-    "The expected number of session history entries was restored.");
-  is(restoredTabState.index, maxBack2 + 1, "The restored tab-state index is correct");
-
-  indexURLOffset = (maxEntries - 1) - maxBack2;
-  for (let i = maxBack2 - 2; i <= maxBack2; i++) {
-    is(restoredTabState.entries[i].url, baseURL + (i + indexURLOffset),
-        "URL of restored entry matches the expected URL.");
-  }
-
-  gBrowser.removeTab(tab);
-});
--- a/browser/components/sessionstore/test/head.js
+++ b/browser/components/sessionstore/test/head.js
@@ -234,17 +234,17 @@ function waitForTopic(aTopic, aTimeout, 
   observing = true;
   Services.obs.addObserver(observer, aTopic, false);
 }
 
 /**
  * Wait until session restore has finished collecting its data and is
  * has written that data ("sessionstore-state-write-complete").
  *
- * @param {function} aCallback If sessionstore-state-write is sent
+ * @param {function} aCallback If sessionstore-state-write-complete is sent
  * within buffering interval + 100 ms, the callback is passed |true|,
  * otherwise, it is passed |false|.
  */
 function waitForSaveState(aCallback) {
   let timeout = 100 +
     Services.prefs.getIntPref("browser.sessionstore.interval");
   return waitForTopic("sessionstore-state-write-complete", timeout, aCallback);
 }
--- a/browser/components/sessionstore/test/unit/test_backup_once.js
+++ b/browser/components/sessionstore/test/unit/test_backup_once.js
@@ -74,34 +74,37 @@ function promise_check_exist(path, shoul
     }
   });
 }
 
 function promise_check_contents(path, expect) {
   return Task.spawn(function*() {
     do_print("Checking whether " + path + " has the right contents");
     let actual = yield OS.File.read(path, { encoding: "utf-8"});
-    if (actual != expect) {
-      throw new Error("File " + path + " should contain\n\t" + expect + "\nbut contains " + actual);
-    }
+    Assert.deepEqual(JSON.parse(actual), expect, `File ${path} contains the expected data.`);
   });
 }
 
+function generateFileContents(id) {
+  let url = `http://example.com/test_backup_once#${id}_${Math.random()}`;
+  return {windows: [{tabs: [{entries: [{url}], index: 1}]}]}
+}
+
 // Write to the store, and check that it creates:
 // - $Path.recovery with the new data
 // - $Path.nextUpgradeBackup with the old data
 add_task(function* test_first_write_backup() {
-  let initial_content = "initial content " + Math.random();
-  let new_content = "test_1 " + Math.random();
+  let initial_content = generateFileContents("initial");
+  let new_content = generateFileContents("test_1");
 
   do_print("Before the first write, none of the files should exist");
   yield promise_check_exist(Paths.backups, false);
 
   yield File.makeDir(Paths.backups);
-  yield File.writeAtomic(Paths.clean, initial_content, { encoding: "utf-8" });
+  yield File.writeAtomic(Paths.clean, JSON.stringify(initial_content), { encoding: "utf-8" });
   yield SessionFile.write(new_content);
 
   do_print("After first write, a few files should have been created");
   yield promise_check_exist(Paths.backups, true);
   yield promise_check_exist(Paths.clean, false);
   yield promise_check_exist(Paths.cleanBackup, true);
   yield promise_check_exist(Paths.recovery, true);
   yield promise_check_exist(Paths.recoveryBackup, false);
@@ -111,18 +114,19 @@ add_task(function* test_first_write_back
   yield promise_check_contents(Paths.nextUpgradeBackup, initial_content);
 });
 
 // Write to the store again, and check that
 // - $Path.clean is not written
 // - $Path.recovery contains the new data
 // - $Path.recoveryBackup contains the previous data
 add_task(function* test_second_write_no_backup() {
-  let new_content = "test_2 " + Math.random();
+  let new_content = generateFileContents("test_2");
   let previous_backup_content = yield File.read(Paths.recovery, { encoding: "utf-8" });
+  previous_backup_content = JSON.parse(previous_backup_content);
 
   yield OS.File.remove(Paths.cleanBackup);
 
   yield SessionFile.write(new_content);
 
   yield promise_check_exist(Paths.backups, true);
   yield promise_check_exist(Paths.clean, false);
   yield promise_check_exist(Paths.cleanBackup, false);
@@ -131,21 +135,19 @@ add_task(function* test_second_write_no_
 
   yield promise_check_contents(Paths.recovery, new_content);
   yield promise_check_contents(Paths.recoveryBackup, previous_backup_content);
 });
 
 // Make sure that we create $Paths.clean and remove $Paths.recovery*
 // upon shutdown
 add_task(function* test_shutdown() {
-  let output = "test_3 " + Math.random();
+  let output = generateFileContents("test_3");
 
   yield File.writeAtomic(Paths.recovery, "I should disappear");
   yield File.writeAtomic(Paths.recoveryBackup, "I should also disappear");
 
   yield SessionWorker.post("write", [output, { isFinalWrite: true, performShutdownCleanup: true}]);
 
   do_check_false((yield File.exists(Paths.recovery)));
   do_check_false((yield File.exists(Paths.recoveryBackup)));
-  let input = yield File.read(Paths.clean, { encoding: "utf-8"});
-  do_check_eq(input, output);
-
+  yield promise_check_contents(Paths.clean, output);
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_shutdown_cleanup.js
@@ -0,0 +1,151 @@
+"use strict";
+
+/**
+ * This test ensures that we correctly clean up the session state before
+ * writing to disk a last time on shutdown. For now it only tests that each
+ * tab's shistory is capped to a maximum number of preceding and succeeding
+ * entries.
+ */
+
+const {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+const {SessionWorker} = Cu.import("resource:///modules/sessionstore/SessionWorker.jsm", {});
+
+const profd = do_get_profile();
+const {SessionFile} = Cu.import("resource:///modules/sessionstore/SessionFile.jsm", {});
+const {Paths} = SessionFile;
+
+const {OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+const {File} = OS;
+
+const MAX_ENTRIES = 9;
+const URL = "http://example.com/#";
+
+// We need a XULAppInfo to initialize SessionFile
+let XULAppInfo = {
+  vendor: "Mozilla",
+  name: "SessionRestoreTest",
+  ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
+  version: "1",
+  appBuildID: "2007010101",
+  platformVersion: "",
+  platformBuildID: "2007010101",
+  inSafeMode: false,
+  logConsoleErrors: true,
+  OS: "XPCShell",
+  XPCOMABI: "noarch-spidermonkey",
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIXULAppInfo,
+    Ci.nsIXULRuntime,
+  ])
+};
+
+let XULAppInfoFactory = {
+  createInstance: function (outer, iid) {
+    if (outer != null)
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    return XULAppInfo.QueryInterface(iid);
+  }
+};
+
+let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"),
+                          "XULAppInfo", "@mozilla.org/xre/app-info;1",
+                          XULAppInfoFactory);
+
+add_task(function* setup() {
+  let source = do_get_file("data/sessionstore_valid.js");
+  source.copyTo(profd, "sessionstore.js");
+
+  // Finish SessionFile initialization.
+  yield SessionFile.read();
+
+  // Reset prefs on cleanup.
+  do_register_cleanup(() => {
+    Services.prefs.clearUserPref("browser.sessionstore.max_serialize_back");
+    Services.prefs.clearUserPref("browser.sessionstore.max_serialize_forward");
+  });
+});
+
+function createSessionState(index) {
+  // Generate the tab state entries and set the one-based
+  // tab-state index to the middle session history entry.
+  let tabState = {entries: [], index};
+  for (let i = 0; i < MAX_ENTRIES; i++) {
+    tabState.entries.push({url: URL + i});
+  }
+
+  return {windows: [{tabs: [tabState]}]};
+}
+
+function* setMaxBackForward(back, fwd) {
+  Services.prefs.setIntPref("browser.sessionstore.max_serialize_back", back);
+  Services.prefs.setIntPref("browser.sessionstore.max_serialize_forward", fwd);
+  yield SessionFile.read();
+}
+
+function* writeAndParse(state, path, options = {}) {
+  yield SessionWorker.post("write", [state, options]);
+  return JSON.parse(yield File.read(path, {encoding: "utf-8"}));
+}
+
+add_task(function* test_shistory_cap_none() {
+  let state = createSessionState(5);
+
+  // Don't limit the number of shistory entries.
+  yield setMaxBackForward(-1, -1);
+
+  // Check that no caps are applied.
+  let diskState = yield writeAndParse(state, Paths.clean, {isFinalWrite: true});
+  Assert.deepEqual(state, diskState, "no cap applied");
+});
+
+add_task(function* test_shistory_cap_middle() {
+  let state = createSessionState(5);
+  yield setMaxBackForward(2, 3);
+
+  // Cap is only applied on clean shutdown.
+  let diskState = yield writeAndParse(state, Paths.recovery);
+  Assert.deepEqual(state, diskState, "no cap applied");
+
+  // Check that the right number of shistory entries was discarded
+  // and the shistory index updated accordingly.
+  diskState = yield writeAndParse(state, Paths.clean, {isFinalWrite: true});
+  let tabState = state.windows[0].tabs[0];
+  tabState.entries = tabState.entries.slice(2, 8);
+  tabState.index = 3;
+  Assert.deepEqual(state, diskState, "cap applied");
+});
+
+add_task(function* test_shistory_cap_lower_bound() {
+  let state = createSessionState(1);
+  yield setMaxBackForward(5, 5);
+
+  // Cap is only applied on clean shutdown.
+  let diskState = yield writeAndParse(state, Paths.recovery);
+  Assert.deepEqual(state, diskState, "no cap applied");
+
+  // Check that the right number of shistory entries was discarded.
+  diskState = yield writeAndParse(state, Paths.clean, {isFinalWrite: true});
+  let tabState = state.windows[0].tabs[0];
+  tabState.entries = tabState.entries.slice(0, 6);
+  Assert.deepEqual(state, diskState, "cap applied");
+});
+
+add_task(function* test_shistory_cap_upper_bound() {
+  let state = createSessionState(MAX_ENTRIES);
+  yield setMaxBackForward(5, 5);
+
+  // Cap is only applied on clean shutdown.
+  let diskState = yield writeAndParse(state, Paths.recovery);
+  Assert.deepEqual(state, diskState, "no cap applied");
+
+  // Check that the right number of shistory entries was discarded
+  // and the shistory index updated accordingly.
+  diskState = yield writeAndParse(state, Paths.clean, {isFinalWrite: true});
+  let tabState = state.windows[0].tabs[0];
+  tabState.entries = tabState.entries.slice(3);
+  tabState.index = 6;
+  Assert.deepEqual(state, diskState, "cap applied");
+});
--- a/browser/components/sessionstore/test/unit/xpcshell.ini
+++ b/browser/components/sessionstore/test/unit/xpcshell.ini
@@ -4,12 +4,13 @@ tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files =
   data/sessionCheckpoints_all.json
   data/sessionstore_invalid.js
   data/sessionstore_valid.js
 
 [test_backup_once.js]
+[test_histogram_corrupt_files.js]
+[test_shutdown_cleanup.js]
 [test_startup_nosession_async.js]
 [test_startup_session_async.js]
 [test_startup_invalid_session.js]
-[test_histogram_corrupt_files.js]
--- a/browser/devtools/commandline/commands-index.js
+++ b/browser/devtools/commandline/commands-index.js
@@ -1,17 +1,64 @@
 /* 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 gcli = require("gcli/index");
+const { createSystem, connectFront, disconnectFront } = require("gcli/system");
+const { GcliFront } = require("devtools/server/actors/gcli");
+
+/**
+ * This is the basic list of modules that should be loaded into each
+ * requisition instance whether server side or client side
+ */
+exports.baseModules = [
+  "gcli/types/delegate",
+  "gcli/types/selection",
+  "gcli/types/array",
+
+  "gcli/types/boolean",
+  "gcli/types/command",
+  "gcli/types/date",
+  "gcli/types/file",
+  "gcli/types/javascript",
+  "gcli/types/node",
+  "gcli/types/number",
+  "gcli/types/resource",
+  "gcli/types/setting",
+  "gcli/types/string",
+  "gcli/types/union",
+  "gcli/types/url",
 
-const commandModules = [
+  "gcli/fields/fields",
+  "gcli/fields/delegate",
+  "gcli/fields/selection",
+
+  "gcli/ui/focus",
+  "gcli/ui/intro",
+
+  "gcli/converters/converters",
+  "gcli/converters/basic",
+  "gcli/converters/terminal",
+
+  "gcli/languages/command",
+  "gcli/languages/javascript",
+
+  "gcli/commands/clear",
+  "gcli/commands/context",
+  "gcli/commands/help",
+  "gcli/commands/pref",
+];
+
+/**
+ * Some commands belong to a tool (see getToolModules). This is a list of the
+ * modules that are *not* owned by a tool.
+ */
+exports.devtoolsModules = [
   "devtools/tilt/tilt-commands",
   "gcli/commands/addon",
   "gcli/commands/appcache",
   "gcli/commands/calllog",
   "gcli/commands/cmd",
   "gcli/commands/cookie",
   "gcli/commands/csscoverage",
   "gcli/commands/folder",
@@ -23,20 +70,88 @@ const commandModules = [
   "gcli/commands/pagemod",
   "gcli/commands/paintflashing",
   "gcli/commands/restart",
   "gcli/commands/rulers",
   "gcli/commands/screenshot",
   "gcli/commands/tools",
 ];
 
-gcli.addItemsByModule(commandModules, { delayedLoad: true });
+/**
+ * Register commands from tools with 'command: [ "some/module" ]' definitions.
+ * The map/reduce incantation squashes the array of arrays to a single array.
+ */
+const defaultTools = require("definitions").defaultTools;
+exports.devtoolsToolModules = defaultTools.map(def => def.commands || [])
+                                 .reduce((prev, curr) => prev.concat(curr), []);
+
+/**
+ * Add modules to a system for use in a content process (but don't call load)
+ */
+exports.addAllItemsByModule = function(system) {
+  system.addItemsByModule(exports.baseModules, { delayedLoad: true });
+  system.addItemsByModule(exports.devtoolsModules, { delayedLoad: true });
+  system.addItemsByModule(exports.devtoolsToolModules, { delayedLoad: true });
+
+  const { mozDirLoader } = require("gcli/commands/cmd");
+  system.addItemsByModule("mozcmd", { delayedLoad: true, loader: mozDirLoader });
+};
+
+/**
+ * This is WeakMap<Target, Links> where Links is an object that looks like
+ *   { refs: number, promise: Promise<System>, front: GcliFront }
+ */
+var linksForTarget = new WeakMap();
+
+/**
+ * The toolbox uses the following properties on a command to allow it to be
+ * added to the toolbox toolbar
+ */
+var customProperties = [ "buttonId", "buttonClass", "tooltipText" ];
 
-const defaultTools = require("main").defaultTools;
-for (let definition of defaultTools) {
-  if (definition.commands) {
-    gcli.addItemsByModule(definition.commands, { delayedLoad: true });
+/**
+ * Create a system which connects to a GCLI in a remote target
+ * @return Promise<System> for the given target
+ */
+exports.getSystem = function(target) {
+  const existingLinks = linksForTarget.get(target);
+  if (existingLinks != null) {
+    existingLinks.refs++;
+    return existingLinks.promise;
   }
-}
+
+  const system = createSystem({ location: "client" });
+
+  exports.addAllItemsByModule(system);
+
+  // Load the client system
+  const links = {
+    refs: 1,
+    system,
+    promise: system.load().then(() => {
+      return GcliFront.create(target).then(front => {
+        links.front = front;
+        return connectFront(system, front, customProperties).then(() => system);
+      });
+    })
+  };
 
-const { mozDirLoader } = require("gcli/commands/cmd");
+  linksForTarget.set(target, links);
+  return links.promise;
+};
 
-gcli.addItemsByModule("mozcmd", { delayedLoad: true, loader: mozDirLoader });
+/**
+ * Someone that called getSystem doesn't need it any more, so decrement the
+ * count of users of the system for that target, and destroy if needed
+ */
+exports.releaseSystem = function(target) {
+  const links = linksForTarget.get(target);
+  if (links == null) {
+    throw new Error("releaseSystem called for unknown target");
+  }
+
+  links.refs--;
+  if (links.refs === 0) {
+    disconnectFront(links.system, links.front);
+    links.system.destroy();
+    linksForTarget.delete(target);
+  }
+};
--- a/browser/devtools/commandline/test/browser_cmd_addon.js
+++ b/browser/devtools/commandline/test/browser_cmd_addon.js
@@ -2,17 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the addon commands works as they should
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
-function spawnTest() {
+function* spawnTest() {
   let options = yield helpers.openTab("about:blank");
   yield helpers.openToolbar(options);
 
   yield helpers.audit(options, [
     {
       setup: 'addon list dictionary',
       check: {
         input:  'addon list dictionary',
--- a/browser/devtools/commandline/test/browser_cmd_appcache_invalid.js
+++ b/browser/devtools/commandline/test/browser_cmd_appcache_invalid.js
@@ -6,17 +6,17 @@
 
 const TEST_URI = "http://sub1.test1.example.com/browser/browser/devtools/commandline/" +
                  "test/browser_cmd_appcache_invalid_index.html";
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
-function spawnTest() {
+function* spawnTest() {
   let lines = [
     'Manifest has a character encoding of ISO-8859-1. Manifests must have the ' +
       'utf-8 character encoding.',
     'The first line of the manifest must be "CACHE MANIFEST" at line 1.',
     '"CACHE MANIFEST" is only valid on the first line but was found at line 3.',
     'images/sound-icon.png points to a resource that is not available at line 9.',
     'images/background.png points to a resource that is not available at line 10.',
     '/checking.cgi points to a resource that is not available at line 13.',
--- a/browser/devtools/commandline/test/browser_cmd_appcache_valid.js
+++ b/browser/devtools/commandline/test/browser_cmd_appcache_valid.js
@@ -5,17 +5,17 @@
 
 const TEST_URI = "http://sub1.test2.example.com/browser/browser/devtools/" +
                  "commandline/test/browser_cmd_appcache_valid_index.html";
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
-function spawnTest() {
+function* spawnTest() {
   let options = yield helpers.openTab(TEST_URI);
   yield helpers.openToolbar(options);
 
   info("adding cache listener.");
 
   // Wait for site to be cached.
   yield helpers.listenOnce(gBrowser.contentWindow.applicationCache, 'cached');
 
--- a/browser/devtools/commandline/test/browser_cmd_calllog.js
+++ b/browser/devtools/commandline/test/browser_cmd_calllog.js
@@ -3,17 +3,17 @@
 
 // Tests that the calllog commands works as they should
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-calllog";
 
 let tests = {};
 
 function test() {
-  return Task.spawn(function() {
+  return Task.spawn(function*() {
     let options = yield helpers.openTab(TEST_URI);
     yield helpers.openToolbar(options);
 
     yield helpers.runTests(options, tests);
 
     yield helpers.closeToolbar(options);
     yield helpers.closeTab(options);
   }).then(finish, helpers.handleError);
--- a/browser/devtools/commandline/test/browser_cmd_calllog_chrome.js
+++ b/browser/devtools/commandline/test/browser_cmd_calllog_chrome.js
@@ -3,17 +3,17 @@
 
 // Tests that the calllog commands works as they should
 
 const TEST_URI = "data:text/html;charset=utf-8,cmd-calllog-chrome";
 
 let tests = {};
 
 function test() {
-  return Task.spawn(function() {
+  return Task.spawn(function*() {
     let options = yield helpers.openTab(TEST_URI);
     yield helpers.openToolbar(options);
 
     yield helpers.runTests(options, tests);
 
     yield helpers.closeToolbar(options);
     yield helpers.closeTab(options);
   }).then(finish, helpers.handleError);
--- a/browser/devtools/commandline/test/browser_cmd_commands.js
+++ b/browser/devtools/commandline/test/browser_cmd_commands.js
@@ -4,17 +4,17 @@
 // Test various GCLI commands
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-commands";
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
-function spawnTest() {
+function* spawnTest() {
   let options = yield helpers.openTab(TEST_URI);
   yield helpers.openToolbar(options);
 
   let subjectPromise = helpers.observeOnce("web-console-created");
 
   helpers.audit(options, [
     {
       setup: "console open",
@@ -44,17 +44,17 @@ function spawnTest() {
   yield oncePromise;
 
   let labels = hud.outputNode.querySelectorAll(".message");
   is(labels.length, 0, "no output in console");
 
   yield helpers.audit(options, [
     {
       setup: "console close",
-      exec: { output: true }
+      exec: { output: "" }
     }
   ]);
 
   ok(!HUDService.getHudReferenceById(hud.hudId), "console closed");
 
   yield helpers.closeToolbar(options);
   yield helpers.closeTab(options);
 }
--- a/browser/devtools/commandline/test/browser_cmd_csscoverage_startstop.js
+++ b/browser/devtools/commandline/test/browser_cmd_csscoverage_startstop.js
@@ -28,28 +28,31 @@ add_task(function*() {
   yield helpers.closeToolbar(options);
   yield helpers.closeTab(options);
 });
 
 /**
  * Visit all the pages in the test
  */
 function* navigate(usage, options) {
-  yield usage.start();
+  yield usage.start(options.chromeWindow, options.target);
 
   ok(usage.isRunning(), "csscoverage is running");
 
+  let load1Promise = helpers.listenOnce(options.browser, "load", true);
+
   yield helpers.navigate(PAGE_1, options);
 
   // Wait for the test pages to auto-cycle
-  let ev = yield helpers.listenOnce(options.browser, "load", true);
-  is(ev.target.location.href, PAGE_1, "page 1 loaded");
+  yield load1Promise;
+  is(options.window.location.href, PAGE_1, "page 1 loaded");
 
-  ev = yield helpers.listenOnce(options.browser, "load", true);
-  is(ev.target.location.href, PAGE_3, "page 3 loaded");
+  // Page 2 is a frame in page 1. JS in the page navigates to page 3.
+  yield helpers.listenOnce(options.browser, "load", true);
+  is(options.window.location.href, PAGE_3, "page 3 loaded");
 
   yield usage.stop();
 
   ok(!usage.isRunning(), "csscoverage not is running");
 }
 
 /**
  * Check the expected pages have been visited
--- a/browser/devtools/commandline/test/browser_cmd_jsb.js
+++ b/browser/devtools/commandline/test/browser_cmd_jsb.js
@@ -5,17 +5,17 @@
 
 const TEST_URI = "http://example.com/browser/browser/devtools/commandline/" +
                  "test/browser_cmd_jsb_script.jsi";
 
 function test() {
   return Task.spawn(testTask).then(finish, helpers.handleError);
 }
 
-function testTask() {
+function* testTask() {
   let options = yield helpers.openTab("about:blank");
   yield helpers.openToolbar(options);
 
   let notifyPromise = wwNotifyOnce();
 
   helpers.audit(options, [
     {
       setup: 'jsb',
@@ -24,17 +24,20 @@ function testTask() {
         hints:     ' <url> [options]',
         markup: 'VVV',
         status: 'ERROR'
       }
     },
     {
       setup: 'jsb ' + TEST_URI,
       // Should result in a new scratchpad window
-      exec: { }
+      exec: {
+        output: '',
+        error: false
+      }
     }
   ]);
 
   let { subject } = yield notifyPromise;
   let scratchpadWin = subject.QueryInterface(Ci.nsIDOMWindow);
   yield helpers.listenOnce(scratchpadWin, "load");
 
   let scratchpad = scratchpadWin.Scratchpad;
--- a/browser/devtools/commandline/test/browser_cmd_media.js
+++ b/browser/devtools/commandline/test/browser_cmd_media.js
@@ -69,17 +69,17 @@ let tests = {
           is(style.backgroundColor, "rgb(255, 255, 255)", "media reset");
         }
       }
     ]);
   }
 };
 
 function test() {
-  return Task.spawn(function() {
+  return Task.spawn(function*() {
     let options = yield helpers.openTab(TEST_URI);
     yield helpers.openToolbar(options);
 
     yield helpers.runTests(options, tests);
 
     yield helpers.closeToolbar(options);
     yield helpers.closeTab(options);
   }).then(finish, helpers.handleError);
--- a/browser/devtools/commandline/test/browser_cmd_pagemod_export.js
+++ b/browser/devtools/commandline/test/browser_cmd_pagemod_export.js
@@ -5,17 +5,17 @@
 
 const TEST_URI = "http://example.com/browser/browser/devtools/commandline/"+
                  "test/browser_cmd_pagemod_export.html";
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
-function spawnTest() {
+function* spawnTest() {
   let options = yield helpers.openTab(TEST_URI);
   yield helpers.openToolbar(options);
 
   const documentElement = options.document.documentElement;
   const initialHtml = documentElement.innerHTML;
   function resetContent() {
     options.document.documentElement.innerHTML = initialHtml;
   }
@@ -297,32 +297,34 @@ function spawnTest() {
       check: {
         input:  'pagemod remove attribute',
         hints:                          ' <searchAttributes> <searchElements> [root] [ignoreCase]',
         markup: 'VVVVVVVVVVVVVVVVVVVVVVVV',
         status: 'ERROR',
         args: {
           searchAttributes: { value: undefined, status: 'INCOMPLETE' },
           searchElements: { value: undefined, status: 'INCOMPLETE' },
-          root: { value: undefined },
+          // root: { value: undefined }, // 'root' is a node which is remote
+                                         // so we can't see the value in tests
           ignoreCase: { value: false },
         }
       },
     },
     {
       setup: 'pagemod remove attribute foo bar',
       check: {
         input:  'pagemod remove attribute foo bar',
         hints:                                  ' [root] [ignoreCase]',
         markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
         status: 'VALID',
         args: {
           searchAttributes: { value: 'foo' },
           searchElements: { value: 'bar' },
-          root: { value: undefined },
+          // root: { value: undefined }, // 'root' is a node which is remote
+                                         // so we can't see the value in tests
           ignoreCase: { value: false },
         }
       },
       post: function() {
         return new Promise(resolve => {
           executeSoon(resolve);
         });
       }
--- a/browser/devtools/commandline/test/browser_cmd_pref1.js
+++ b/browser/devtools/commandline/test/browser_cmd_pref1.js
@@ -8,17 +8,17 @@ let prefBranch = Cc["@mozilla.org/prefer
                     .QueryInterface(Ci.nsIPrefBranch2);
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-pref1";
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
-function spawnTest() {
+function* spawnTest() {
   let options = yield helpers.openTab(TEST_URI);
   yield helpers.openToolbar(options);
 
   let tiltEnabledOrig = prefBranch.getBoolPref("devtools.tilt.enabled");
   info("originally: devtools.tilt.enabled = " + tiltEnabledOrig);
 
   yield helpers.audit(options, [
     {
--- a/browser/devtools/commandline/test/browser_cmd_pref2.js
+++ b/browser/devtools/commandline/test/browser_cmd_pref2.js
@@ -8,17 +8,17 @@ let prefBranch = Cc["@mozilla.org/prefer
                     .QueryInterface(Ci.nsIPrefBranch2);
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-pref2";
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
-function spawnTest() {
+function* spawnTest() {
   let options = yield helpers.openTab(TEST_URI);
   yield helpers.openToolbar(options);
 
   let tabSizeOrig = prefBranch.getIntPref("devtools.editor.tabsize");
   info("originally: devtools.editor.tabsize = " + tabSizeOrig);
 
   yield helpers.audit(options, [
     {
--- a/browser/devtools/commandline/test/browser_cmd_pref3.js
+++ b/browser/devtools/commandline/test/browser_cmd_pref3.js
@@ -11,17 +11,17 @@ let supportsString = Cc["@mozilla.org/su
                       .createInstance(Ci.nsISupportsString);
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-pref3";
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
-function spawnTest() {
+function* spawnTest() {
   let options = yield helpers.openTab(TEST_URI);
   yield helpers.openToolbar(options);
 
   let remoteHostOrig = prefBranch.getComplexValue("devtools.debugger.remote-host",
                                                   Ci.nsISupportsString).data;
   info("originally: devtools.debugger.remote-host = " + remoteHostOrig);
 
   yield helpers.audit(options, [
--- a/browser/devtools/commandline/test/browser_cmd_screenshot.js
+++ b/browser/devtools/commandline/test/browser_cmd_screenshot.js
@@ -6,17 +6,17 @@ const TEST_URI = "http://example.com/bro
                  "test/browser_cmd_screenshot.html";
 
 let FileUtils = (Cu.import("resource://gre/modules/FileUtils.jsm", {})).FileUtils;
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
-function spawnTest() {
+function* spawnTest() {
   waitForExplicitFinish();
 
   info("RUN TEST: non-private window");
   let normWin = yield addWindow({ private: false });
   yield addTabWithToolbarRunTests(normWin);
   normWin.close();
 
   info("RUN TEST: private window");
@@ -76,21 +76,16 @@ function addTabWithToolbarRunTests(win) 
       },
     },
     {
       setup: 'screenshot --selector img#testImage',
       check: {
         input:  'screenshot --selector img#testImage',
         markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
         status: 'VALID',
-        args: {
-          selector: {
-            value: options.window.document.getElementById("testImage")
-          },
-        }
       },
     },
   ]);
 
   // Test capture to file
   let file = FileUtils.getFile("TmpD", [ "TestScreenshotFile.png" ]);
 
   yield helpers.audit(options, [
--- a/browser/devtools/commandline/test/browser_cmd_settings.js
+++ b/browser/devtools/commandline/test/browser_cmd_settings.js
@@ -11,24 +11,28 @@ let supportsString = Cc["@mozilla.org/su
                       .createInstance(Ci.nsISupportsString);
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-settings";
 
 function test() {
   return Task.spawn(spawnTest).then(finish, helpers.handleError);
 }
 
-function spawnTest() {
+function* spawnTest() {
   // Setup
   let options = yield helpers.openTab(TEST_URI);
 
-  require("devtools/commandline/commands-index");
-  let gcli = require("gcli/index");
-  yield gcli.load();
-  let settings = gcli.settings;
+  const { createSystem } = require("gcli/system");
+  const system = createSystem({ location: "server" });
+
+  const gcliInit = require("devtools/commandline/commands-index");
+  gcliInit.addAllItemsByModule(system);
+  yield system.load();
+
+  let settings = system.settings;
 
   let hideIntroEnabled = settings.get("devtools.gcli.hideIntro");
   let tabSize = settings.get("devtools.editor.tabsize");
   let remoteHost = settings.get("devtools.debugger.remote-host");
 
   let hideIntroOrig = prefBranch.getBoolPref("devtools.gcli.hideIntro");
   let tabSizeOrig = prefBranch.getIntPref("devtools.editor.tabsize");
   let remoteHostOrig = prefBranch.getComplexValue(
--- a/browser/devtools/commandline/test/browser_gcli_async.js
+++ b/browser/devtools/commandline/test/browser_gcli_async.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testAsync.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_async.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testBasic = function(options) {
   return helpers.audit(options, [
     {
       setup:    'tsslo',
       check: {
         input:  'tsslo',
@@ -69,17 +54,16 @@ exports.testBasic = function(options) {
         predictions: [
           'Shalom', 'Namasté', 'Hallo', 'Dydd-da', 'Chào', 'Hej',
           'Saluton', 'Sawubona'
         ],
         unassigned: [ ],
         args: {
           command: { name: 'tsslow' },
           hello: {
-            value: undefined,
             arg: '',
             status: 'INCOMPLETE'
           },
         }
       }
     },
     {
       setup:    'tsslow S',
@@ -90,17 +74,16 @@ exports.testBasic = function(options) {
         cursor: 8,
         current: 'hello',
         status: 'ERROR',
         predictions: [ 'Shalom', 'Saluton', 'Sawubona', 'Namasté' ],
         unassigned: [ ],
         args: {
           command: { name: 'tsslow' },
           hello: {
-            value: undefined,
             arg: ' S',
             status: 'INCOMPLETE'
           },
         }
       }
     },
     {
       setup:    'tsslow S<TAB>',
@@ -111,17 +94,16 @@ exports.testBasic = function(options) {
         cursor: 14,
         current: 'hello',
         status: 'VALID',
         predictions: [ 'Shalom' ],
         unassigned: [ ],
         args: {
           command: { name: 'tsslow' },
           hello: {
-            value: 'Shalom',
             arg: ' Shalom ',
             status: 'VALID',
             message: ''
           },
         }
       }
     }
   ]);
--- a/browser/devtools/commandline/test/browser_gcli_canon.js
+++ b/browser/devtools/commandline/test/browser_gcli_canon.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testCanon.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_canon.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 var Commands = require('gcli/commands/commands').Commands;
 
 var startCount;
 var events;
 
 var commandsChange = function(ev) {
@@ -214,32 +199,53 @@ exports.testAltCommands = function(optio
 
   var tss = {
     name: 'tss',
     params: [
       { name: 'str', type: 'string' },
       { name: 'num', type: 'number' },
       { name: 'opt', type: { name: 'selection', data: [ '1', '2', '3' ] } },
     ],
+    customProp1: 'localValue',
+    customProp2: true,
+    customProp3: 42,
     exec: function(args, context) {
       return context.commandName + ':' +
               args.str + ':' + args.num + ':' + args.opt;
     }
   };
   altCommands.add(tss);
 
   var commandSpecs = altCommands.getCommandSpecs();
   assert.is(JSON.stringify(commandSpecs),
             '[{"item":"command","name":"tss","params":[' +
               '{"name":"str","type":"string"},' +
               '{"name":"num","type":"number"},' +
               '{"name":"opt","type":{"name":"selection","data":["1","2","3"]}}' +
             '],"isParent":false}]',
             'JSON.stringify(commandSpecs)');
 
+  var customProps = [ 'customProp1', 'customProp2', 'customProp3', ];
+  var commandSpecs2 = altCommands.getCommandSpecs(customProps);
+  assert.is(JSON.stringify(commandSpecs2),
+            '[{' +
+              '"item":"command",' +
+              '"name":"tss",' +
+              '"params":[' +
+                '{"name":"str","type":"string"},' +
+                '{"name":"num","type":"number"},' +
+                '{"name":"opt","type":{"name":"selection","data":["1","2","3"]}}' +
+              '],' +
+              '"isParent":false,' +
+              '"customProp1":"localValue",' +
+              '"customProp2":true,' +
+              '"customProp3":42' +
+            '}]',
+            'JSON.stringify(commandSpecs)');
+
   var remoter = function(args, context) {
     assert.is(context.commandName, 'tss', 'commandName is tss');
 
     var cmd = altCommands.get(context.commandName);
     return cmd.exec(args, context);
   };
 
   commands.addProxyCommands(commandSpecs, remoter, 'proxy', 'test');
--- a/browser/devtools/commandline/test/browser_gcli_cli1.js
+++ b/browser/devtools/commandline/test/browser_gcli_cli1.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testCli1.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_cli1.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 
 exports.testBlank = function(options) {
   return helpers.audit(options, [
     {
       setup:    '',
       check: {
@@ -263,17 +248,16 @@ exports.testTsv = function(options) {
             arg: '',
             status: 'INCOMPLETE',
             message: 'Value required for \'optionValue\'.'
           }
         }
       }
     },
     {
-      skipRemainingIf: options.isNoDom,
       name: '|tsv option',
       setup: function() {
         return helpers.setInput(options, 'tsv option', 0);
       },
       check: {
         input:  'tsv option',
         hints:            ' <optionValue>',
         markup: 'VVVVEEEEEE',
--- a/browser/devtools/commandline/test/browser_gcli_cli2.js
+++ b/browser/devtools/commandline/test/browser_gcli_cli2.js
@@ -10,55 +10,28 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testCli2.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_cli2.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
-var nodetype = require('gcli/types/node');
-
-exports.setup = function(options) {
-  if (options.window) {
-    nodetype.setDocument(options.window.document);
-  }
-};
-
-exports.shutdown = function(options) {
-  nodetype.unsetDocument();
-};
-
 exports.testSingleString = function(options) {
   return helpers.audit(options, [
     {
       setup:    'tsr',
       check: {
         input:  'tsr',
         hints:     ' <text>',
         markup: 'VVV',
@@ -371,17 +344,16 @@ exports.testSingleFloat = function(optio
         unassigned: [ ],
         args: {
           command: { name: 'tsf' },
           num: { value: 1.5, arg: ' 1.5x', status: 'VALID', message: '' }
         }
       }
     },
     {
-      skipRemainingIf: options.isNoDom,
       name: 'tsf x (cursor=4)',
       setup: function() {
         return helpers.setInput(options, 'tsf x', 4);
       },
       check: {
         input:  'tsf x',
         hints:       '',
         markup: 'VVVVE',
@@ -401,68 +373,59 @@ exports.testSingleFloat = function(optio
           }
         }
       }
     }
   ]);
 };
 
 exports.testElementWeb = function(options) {
-  var inputElement = options.isNoDom ?
-      null :
-      options.window.document.getElementById('gcli-input');
-
   return helpers.audit(options, [
     {
-      skipIf: function gcliInputElementExists() {
-        return inputElement == null;
-      },
-      setup:    'tse #gcli-input',
+      setup:    'tse #gcli-root',
       check: {
-        input:  'tse #gcli-input',
+        input:  'tse #gcli-root',
         hints:                 ' [options]',
-        markup: 'VVVVVVVVVVVVVVV',
-        cursor: 15,
+        markup: 'VVVVVVVVVVVVVV',
+        cursor: 14,
         current: 'node',
         status: 'VALID',
         predictions: [ ],
         unassigned: [ ],
         args: {
           command: { name: 'tse' },
           node: {
-            value: inputElement,
-            arg: ' #gcli-input',
+            arg: ' #gcli-root',
             status: 'VALID',
             message: ''
           },
           nodes: { arg: '', status: 'VALID', message: '' },
           nodes2: { arg: '', status: 'VALID', message: '' },
         }
       }
     }
   ]);
 };
 
 exports.testElement = function(options) {
   return helpers.audit(options, [
     {
-      skipRemainingIf: options.isNoDom,
       setup:    'tse',
       check: {
         input:  'tse',
         hints:     ' <node> [options]',
         markup: 'VVV',
         cursor: 3,
         current: '__command',
         status: 'ERROR',
         predictions: [ 'tse', 'tselarr' ],
         unassigned: [ ],
         args: {
           command: { name: 'tse' },
-          node: { value: undefined, arg: '', status: 'INCOMPLETE' },
+          node: { arg: '', status: 'INCOMPLETE' },
           nodes: { arg: '', status: 'VALID', message: '' },
           nodes2: { arg: '', status: 'VALID', message: '' },
         }
       }
     },
     {
       setup:    'tse #gcli-nomatch',
       check: {
@@ -600,17 +563,17 @@ exports.testNestedCommand = function(opt
         markup: 'IIIV',
         cursor: 4,
         current: '__command',
         status: 'ERROR',
         unassigned: [ ]
       }
     },
     {
-      skipIf: options.isPhantomjs,
+      skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong
       setup:    'tsn x',
       check: {
         input:  'tsn x',
         hints:       ' -> tsn ext',
         markup: 'IIIVI',
         cursor: 5,
         current: '__command',
         status: 'ERROR',
--- a/browser/devtools/commandline/test/browser_gcli_completion1.js
+++ b/browser/devtools/commandline/test/browser_gcli_completion1.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testCompletion1.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_completion1.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testActivate = function(options) {
   return helpers.audit(options, [
     {
       setup: '',
       check: {
         hints: ''
@@ -178,17 +163,17 @@ exports.testActivate = function(options)
     },
     {
       setup: 'tsg b',
       check: {
         hints: 'bb [options]'
       }
     },
     {
-      skipIf: options.isPhantomjs,
+      skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong
       setup: 'tsg d',
       check: {
         hints: ' [options] -> ccc'
       }
     },
     {
       setup: 'tsg aa',
       check: {
--- a/browser/devtools/commandline/test/browser_gcli_completion2.js
+++ b/browser/devtools/commandline/test/browser_gcli_completion2.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testCompletion2.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_completion2.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testLong = function(options) {
   return helpers.audit(options, [
     {
       setup:    'tslong --sel',
       check: {
         input:  'tslong --sel',
@@ -165,17 +150,16 @@ exports.testNoTab = function(options) {
       setup:    'xxxx',
       check: {
         input:  'xxxx',
         markup: 'EEEE',
         hints: ''
       }
     },
     {
-      skipIf: options.isNoDom,
       name: '<TAB>',
       setup: function() {
         // Doing it this way avoids clearing the input buffer
         return helpers.pressTab(options);
       },
       check: {
         input:  'xxxx',
         markup: 'EEEE',
--- a/browser/devtools/commandline/test/browser_gcli_context.js
+++ b/browser/devtools/commandline/test/browser_gcli_context.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testContext.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_context.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testBaseline = function(options) {
   return helpers.audit(options, [
     // These 3 establish a baseline for comparison when we have used the
     // context command
     {
       setup:    'ext',
--- a/browser/devtools/commandline/test/browser_gcli_date.js
+++ b/browser/devtools/commandline/test/browser_gcli_date.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testDate.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_date.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 
 var Status = require('gcli/types/types').Status;
 
 exports.testParse = function(options) {
   var date = options.requisition.system.types.createType('date');
   return date.parseString('now').then(function(conversion) {
@@ -61,25 +46,25 @@ exports.testParse = function(options) {
 
 exports.testMaxMin = function(options) {
   var max = new Date();
   var min = new Date();
   var types = options.requisition.system.types;
   var date = types.createType({ name: 'date', max: max, min: min });
   assert.is(date.getMax(), max, 'max setup');
 
-  var incremented = date.increment(min);
+  var incremented = date.nudge(min, 1);
   assert.is(incremented, max, 'incremented');
 };
 
 exports.testIncrement = function(options) {
   var date = options.requisition.system.types.createType('date');
   return date.parseString('now').then(function(conversion) {
-    var plusOne = date.increment(conversion.value);
-    var minusOne = date.decrement(plusOne);
+    var plusOne = date.nudge(conversion.value, 1);
+    var minusOne = date.nudge(plusOne, -1);
 
     // See comments in testParse
     var gap = new Date().getTime() - minusOne.getTime();
     assert.ok(gap < 60000, 'now is less than a minute away');
   });
 };
 
 exports.testInput = function(options) {
@@ -121,17 +106,17 @@ exports.testInput = function(options) {
             arg: ' 1980-01-03',
             status: 'VALID',
             message: ''
           },
         }
       },
       exec: {
         output: [ /^Exec: tsdate/, /2001/, /1980/ ],
-        type: 'string',
+        type: 'testCommandOutput',
         error: false
       }
     },
     {
       setup:    'tsdate 2001/01/01 1980/01/03',
       check: {
         input:  'tsdate 2001/01/01 1980/01/03',
         hints:                              '',
@@ -167,17 +152,17 @@ exports.testInput = function(options) {
             arg: ' 1980/01/03',
             status: 'VALID',
             message: ''
           },
         }
       },
       exec: {
         output: [ /^Exec: tsdate/, /2001/, /1980/ ],
-        type: 'string',
+        type: 'testCommandOutput',
         error: false
       }
     },
     {
       setup:    'tsdate now today',
       check: {
         input:  'tsdate now today',
         hints:                  '',
@@ -208,17 +193,17 @@ exports.testInput = function(options) {
             arg: ' today',
             status: 'VALID',
             message: ''
           },
         }
       },
       exec: {
         output: [ /^Exec: tsdate/, new Date().getFullYear() ],
-        type: 'string',
+        type: 'testCommandOutput',
         error: false
       }
     },
     {
       setup:    'tsdate yesterday tomorrow',
       check: {
         input:  'tsdate yesterday tomorrow',
         hints:                           '',
@@ -248,28 +233,28 @@ exports.testInput = function(options) {
             arg: ' tomorrow',
             status: 'VALID',
             message: ''
           },
         }
       },
       exec: {
         output: [ /^Exec: tsdate/, new Date().getFullYear() ],
-        type: 'string',
+        type: 'testCommandOutput',
         error: false
       }
     }
   ]);
 };
 
 exports.testIncrDecr = function(options) {
   return helpers.audit(options, [
     {
       // createRequisitionAutomator doesn't fake UP/DOWN well enough
-      skipRemainingIf: options.isNoDom,
+      skipRemainingIf: options.isNode,
       setup:    'tsdate 2001-01-01<UP>',
       check: {
         input:  'tsdate 2001-01-02',
         hints:                    ' <d2>',
         markup: 'VVVVVVVVVVVVVVVVV',
         status: 'ERROR',
         message: '',
         args: {
--- a/browser/devtools/commandline/test/browser_gcli_exec.js
+++ b/browser/devtools/commandline/test/browser_gcli_exec.js
@@ -10,64 +10,28 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testExec.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_exec.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
-var nodetype = require('gcli/types/node');
-
-var mockBody = {
-  style: {}
-};
-
-var mockEmptyNodeList = {
-  length: 0,
-  item: function() { return null; }
-};
-
-var mockRootNodeList = {
-  length: 1,
-  item: function(i) { return mockBody; }
-};
-
-var mockDoc = {
-  querySelectorAll: function(css) {
-    return (css === ':root') ? mockRootNodeList : mockEmptyNodeList;
-  }
-};
 
 exports.testParamGroup = function(options) {
   var tsg = options.requisition.system.commands.get('tsg');
 
   assert.is(tsg.params[0].groupName, null, 'tsg param 0 group null');
   assert.is(tsg.params[1].groupName, 'First', 'tsg param 1 group First');
   assert.is(tsg.params[2].groupName, 'First', 'tsg param 2 group First');
   assert.is(tsg.params[3].groupName, 'Second', 'tsg param 3 group Second');
@@ -116,17 +80,17 @@ exports.testWithHelpers = function(optio
           optionValue: {
             arg: ' 10',
             status: 'VALID',
             message: ''
           }
         }
       },
       exec: {
-        output: 'Exec: tsv optionType=string, optionValue=10'
+        output: 'Exec: tsv optionType=option1 optionValue=10'
       }
     },
     {
       setup:    'tsv option2 10',
       check: {
         input:  'tsv option2 10',
         hints:                '',
         markup: 'VVVVVVVVVVVVVV',
@@ -146,39 +110,39 @@ exports.testWithHelpers = function(optio
           optionValue: {
             arg: ' 10',
             status: 'VALID',
             message: ''
           }
         }
       },
       exec: {
-        output: 'Exec: tsv optionType=number, optionValue=10'
+        output: 'Exec: tsv optionType=option2 optionValue=10'
       }
     },
     // Delegated remote types can't transfer value types so we only test for
     // the value of optionValue when we're local
     {
       skipIf: options.isRemote,
       setup: 'tsv option1 10',
       check: {
         args: { optionValue: { value: '10' } }
       },
       exec: {
-        output: 'Exec: tsv optionType=string, optionValue=10'
+        output: 'Exec: tsv optionType=option1 optionValue=10'
       }
     },
     {
       skipIf: options.isRemote,
       setup: 'tsv option2 10',
       check: {
         args: { optionValue: { value: 10 } }
       },
       exec: {
-        output: 'Exec: tsv optionType=number, optionValue=10'
+        output: 'Exec: tsv optionType=option2 optionValue=10'
       }
     }
   ]);
 };
 
 exports.testExecText = function(options) {
   return helpers.audit(options, [
     {
@@ -223,17 +187,17 @@ exports.testExecText = function(options)
             value: 'fred bloggs',
             arg: ' fred bloggs',
             status: 'VALID',
             message: ''
           }
         }
       },
       exec: {
-        output: 'Exec: tsr text=fred bloggs'
+        output: 'Exec: tsr text=fred\\ bloggs'
       }
     },
     {
       setup:    'tsr "fred bloggs"',
       check: {
         input:  'tsr "fred bloggs"',
         hints:                   '',
         markup: 'VVVVVVVVVVVVVVVVV',
@@ -248,17 +212,17 @@ exports.testExecText = function(options)
             value: 'fred bloggs',
             arg: ' "fred bloggs"',
             status: 'VALID',
             message: ''
           }
         }
       },
       exec: {
-        output: 'Exec: tsr text=fred bloggs'
+        output: 'Exec: tsr text=fred\\ bloggs'
       }
     },
     {
       setup:    'tsr "fred bloggs',
       check: {
         input:  'tsr "fred bloggs',
         hints:                  '',
         markup: 'VVVVVVVVVVVVVVVV',
@@ -273,17 +237,17 @@ exports.testExecText = function(options)
             value: 'fred bloggs',
             arg: ' "fred bloggs',
             status: 'VALID',
             message: ''
           }
         }
       },
       exec: {
-        output: 'Exec: tsr text=fred bloggs'
+        output: 'Exec: tsr text=fred\\ bloggs'
       }
     }
   ]);
 };
 
 exports.testExecBoolean = function(options) {
   return helpers.audit(options, [
     {
@@ -398,74 +362,69 @@ exports.testExecScript = function(option
         cursor: 13,
         current: 'javascript',
         status: 'VALID',
         predictions: [ ],
         unassigned: [ ],
         args: {
           command: { name: 'tsj' },
           javascript: {
-            value: '1 + 1',
             arg: ' { 1 + 1 }',
             status: 'VALID',
             message: ''
           }
         }
       },
       exec: {
         output: 'Exec: tsj javascript=1 + 1'
       }
     }
   ]);
 };
 
 exports.testExecNode = function(options) {
-  var origDoc = nodetype.getDocument();
-  nodetype.setDocument(mockDoc);
-
   return helpers.audit(options, [
     {
-      skipIf: options.isNoDom,
+      skipIf: options.isRemote,
       setup:    'tse :root',
       check: {
         input:  'tse :root',
         hints:           ' [options]',
         markup: 'VVVVVVVVV',
         cursor: 9,
         current: 'node',
         status: 'VALID',
         predictions: [ ],
         unassigned: [ ],
         args: {
           command: { name: 'tse' },
           node: {
-            value: mockBody,
             arg: ' :root',
             status: 'VALID',
             message: ''
           },
           nodes: {
-            value: mockEmptyNodeList,
             arg: '',
             status: 'VALID',
             message: ''
           },
           nodes2: {
-            value: mockEmptyNodeList,
             arg: '',
             status: 'VALID',
             message: ''
           }
         }
       },
       exec: {
         output: /^Exec: tse/
       },
-      post: function() {
-        nodetype.setDocument(origDoc);
+      post: function(output) {
+        assert.is(output.data.args.node, ':root', 'node should be :root');
+        assert.is(output.data.args.nodes, 'Error', 'nodes should be Error');
+        assert.is(output.data.args.nodes2, 'Error', 'nodes2 should be Error');
       }
     }
   ]);
 };
 
 exports.testExecSubCommand = function(options) {
   return helpers.audit(options, [
     {
@@ -547,17 +506,17 @@ exports.testExecArray = function(options
         outputState: 'false:default',
         args: {
           command: { name: 'tselarr' },
           num: { value: '1', arg: ' 1', status: 'VALID', message: '' },
           arr: { /*value:,*/ arg: '{}', status: 'VALID', message: '' },
         }
       },
       exec: {
-        output: 'Exec: tselarr num=1, arr='
+        output: 'Exec: tselarr num=1 arr='
       }
     },
     {
       setup:    'tselarr 1 a',
       check: {
         input:  'tselarr 1 a',
         hints:             '',
         markup: 'VVVVVVVVVVV',
@@ -568,17 +527,17 @@ exports.testExecArray = function(options
         unassigned: [ ],
         args: {
           command: { name: 'tselarr' },
           num: { value: '1', arg: ' 1', status: 'VALID', message: '' },
           arr: { /*value:a,*/ arg: '{ a}', status: 'VALID', message: '' },
         }
       },
       exec: {
-        output: 'Exec: tselarr num=1, arr=a'
+        output: 'Exec: tselarr num=1 arr=a'
       }
     },
     {
       setup:    'tselarr 1 a b',
       check: {
         input:  'tselarr 1 a b',
         hints:               '',
         markup: 'VVVVVVVVVVVVV',
@@ -589,17 +548,17 @@ exports.testExecArray = function(options
         unassigned: [ ],
         args: {
           command: { name: 'tselarr' },
           num: { value: '1', arg: ' 1', status: 'VALID', message: '' },
           arr: { /*value:a,b,*/ arg: '{ a, b}', status: 'VALID', message: '' },
         }
       },
       exec: {
-        output: 'Exec: tselarr num=1, arr=a,b'
+        output: 'Exec: tselarr num=1 arr=a b'
       }
     }
   ]);
 };
 
 exports.testExecMultiple = function(options) {
   return helpers.audit(options, [
     {
@@ -616,17 +575,17 @@ exports.testExecMultiple = function(opti
         args: {
           command: { name: 'tsm' },
           abc: { value: 'a', arg: ' a', status: 'VALID', message: '' },
           txt: { value: '10', arg: ' 10', status: 'VALID', message: '' },
           num: { value: 10, arg: ' 10', status: 'VALID', message: '' },
         }
       },
       exec: {
-        output: 'Exec: tsm abc=a, txt=10, num=10'
+        output: 'Exec: tsm abc=a txt=10 num=10'
       }
     }
   ]);
 };
 
 exports.testExecDefaults = function(options) {
   return helpers.audit(options, [
     {
@@ -646,14 +605,52 @@ exports.testExecDefaults = function(opti
           solo: { value: 'aaa', arg: ' aaa', status: 'VALID', message: '' },
           txt1: { value: undefined, arg: '', status: 'VALID', message: '' },
           bool: { value: false, arg: '', status: 'VALID', message: '' },
           txt2: { value: undefined, arg: '', status: 'VALID', message: '' },
           num: { value: undefined, arg: '', status: 'VALID', message: '' },
         }
       },
       exec: {
-        output: 'Exec: tsg solo=aaa, txt1=null, bool=false, txt2=d, num=42'
+        output: 'Exec: tsg solo=aaa txt1= bool=false txt2=d num=42'
       }
     }
   ]);
+};
 
+exports.testNested = function(options) {
+  var commands = options.requisition.system.commands;
+  commands.add({
+    name: 'nestorama',
+    exec: function(args, context) {
+      return context.updateExec('tsb').then(function(tsbOutput) {
+        return context.updateExec('tsu 6').then(function(tsuOutput) {
+          return JSON.stringify({
+            tsb: tsbOutput.data,
+            tsu: tsuOutput.data
+          });
+        });
+      });
+    }
+  });
+
+  return helpers.audit(options, [
+    {
+      setup: 'nestorama',
+      exec: {
+        output:
+          '{' +
+            '"tsb":{' +
+              '"name":"tsb",' +
+              '"args":{"toggle":"false"}' +
+            '},' +
+            '"tsu":{' +
+              '"name":"tsu",' +
+              '"args":{"num":"6"}' +
+            '}' +
+          '}'
+      },
+      post: function() {
+        commands.remove('nestorama');
+      }
+    }
+  ]);
 };
--- a/browser/devtools/commandline/test/browser_gcli_fail.js
+++ b/browser/devtools/commandline/test/browser_gcli_fail.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFail.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_fail.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testBasic = function(options) {
   return helpers.audit(options, [
     {
       setup: 'tsfail reject',
       exec: {
         output: 'rejected promise',
--- a/browser/devtools/commandline/test/browser_gcli_file.js
+++ b/browser/devtools/commandline/test/browser_gcli_file.js
@@ -10,52 +10,34 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFile.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_file.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 var local = false;
 
 exports.testBasic = function(options) {
   return helpers.audit(options, [
     {
-      // These tests require us to be using node directly or to be in
-      // PhantomJS connected to an execute enabled node server or to be in
-      // firefox.
-      skipRemainingIf: options.isPhantomjs || options.isFirefox,
+      skipRemainingIf: options.isFirefox, // No file implementation in Firefox
       setup:    'tsfile open /',
       check: {
         input:  'tsfile open /',
         hints:               '',
         markup: 'VVVVVVVVVVVVI',
         cursor: 13,
         current: 'p1',
         status: 'ERROR',
--- a/browser/devtools/commandline/test/browser_gcli_fileparser.js
+++ b/browser/devtools/commandline/test/browser_gcli_fileparser.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFileparser.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_fileparser.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 var fileparser = require('gcli/util/fileparser');
 
 var local = false;
 
 exports.testGetPredictor = function(options) {
   if (!options.isNode || !local) {
     assert.log('Skipping tests due to install differences.');
--- a/browser/devtools/commandline/test/browser_gcli_filesystem.js
+++ b/browser/devtools/commandline/test/browser_gcli_filesystem.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFilesystem.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_filesystem.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 var filesystem = require('gcli/util/filesystem');
 
 exports.testSplit = function(options) {
   if (!options.isNode) {
     return;
   }
--- a/browser/devtools/commandline/test/browser_gcli_focus.js
+++ b/browser/devtools/commandline/test/browser_gcli_focus.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFocus.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_focus.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testBasic = function(options) {
   return helpers.audit(options, [
     {
       name: 'exec setup',
       setup: function() {
         // Just check that we've got focus, and everything is clear
--- a/browser/devtools/commandline/test/browser_gcli_history.js
+++ b/browser/devtools/commandline/test/browser_gcli_history.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testHistory.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_history.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 var History = require('gcli/ui/history').History;
 
 exports.testSimpleHistory = function (options) {
   var history = new History({});
   history.add('foo');
   history.add('bar');
   assert.is(history.backward(), 'bar');
--- a/browser/devtools/commandline/test/browser_gcli_incomplete.js
+++ b/browser/devtools/commandline/test/browser_gcli_incomplete.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testIncomplete.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_incomplete.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 
 exports.testBasic = function(options) {
   return helpers.audit(options, [
     {
       setup: 'tsu 2 extra',
       check: {
--- a/browser/devtools/commandline/test/browser_gcli_inputter.js
+++ b/browser/devtools/commandline/test/browser_gcli_inputter.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testInputter.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_inputter.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 var KeyEvent = require('gcli/util/util').KeyEvent;
 
 var latestEvent;
 var latestData;
 
 var outputted = function(ev) {
   latestEvent = ev;
@@ -84,17 +69,17 @@ exports.testOutput = function(options) {
             'tss',
             'terminal should do nothing on RETURN keyDown');
   assert.is(latestEvent, undefined, 'no events this test');
   assert.is(latestData, undefined, 'no data this test');
 
   var ev1 = { keyCode: KeyEvent.DOM_VK_RETURN };
   return terminal.handleKeyUp(ev1).then(function() {
     assert.ok(latestEvent != null, 'events this test');
-    assert.is(latestData, 'Exec: tss ', 'last command is tss');
+    assert.is(latestData.name, 'tss', 'last command is tss');
 
     assert.is(terminal.getInputState().typed,
               '',
               'terminal should exec on RETURN keyUp');
 
     assert.ok(focusManager._recentOutput, 'recent output happened');
 
     var ev2 = { keyCode: KeyEvent.DOM_VK_F1 };
--- a/browser/devtools/commandline/test/browser_gcli_intro.js
+++ b/browser/devtools/commandline/test/browser_gcli_intro.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testIntro.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_intro.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testIntroStatus = function(options) {
   return helpers.audit(options, [
     {
       skipRemainingIf: function commandIntroMissing() {
         return options.requisition.system.commands.get('intro') == null;
       },
@@ -62,17 +47,16 @@ exports.testIntroStatus = function(optio
         typed:  'intro foo',
         markup: 'VVVVVVEEE',
         status: 'ERROR',
         hints: ''
       }
     },
     {
       setup:    'intro',
-      skipIf: options.isNoDom,
       check: {
         typed:  'intro',
         markup: 'VVVVV',
         status: 'VALID',
         hints: ''
       },
       exec: {
         output: [
--- a/browser/devtools/commandline/test/browser_gcli_js.js
+++ b/browser/devtools/commandline/test/browser_gcli_js.js
@@ -10,83 +10,65 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testJs.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_js.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
-var javascript = require('gcli/types/javascript');
-
-var tempWindow;
 
 exports.setup = function(options) {
-  if (options.isNoDom) {
+  if (jsTestDisallowed(options)) {
     return;
   }
 
-  tempWindow = javascript.getGlobalObject();
-  Object.defineProperty(options.window, 'donteval', {
+  // Check that we're not trespassing on 'donteval'
+  var win = options.requisition.environment.window;
+  Object.defineProperty(win, 'donteval', {
     get: function() {
       assert.ok(false, 'donteval should not be used');
+      console.trace();
       return { cant: '', touch: '', 'this': '' };
     },
     enumerable: true,
-    configurable : true
+    configurable: true
   });
-  javascript.setGlobalObject(options.window);
 };
 
 exports.shutdown = function(options) {
-  if (options.isNoDom) {
+  if (jsTestDisallowed(options)) {
     return;
   }
 
-  javascript.setGlobalObject(tempWindow);
-  tempWindow = undefined;
-  delete options.window.donteval;
+  delete options.requisition.environment.window.donteval;
 };
 
-function jsTestAllowed(options) {
-  return options.isRemote || options.isNoDom ||
+function jsTestDisallowed(options) {
+  return options.isRemote || // Altering the environment (which isn't remoted)
+         options.isNode ||
          options.requisition.system.commands.get('{') == null;
 }
 
 exports.testBasic = function(options) {
   return helpers.audit(options, [
     {
-      skipRemainingIf: jsTestAllowed,
+      skipRemainingIf: jsTestDisallowed,
       setup:    '{',
       check: {
         input:  '{',
         hints:   '',
         markup: 'V',
         cursor: 1,
         current: 'javascript',
         status: 'ERROR',
@@ -231,17 +213,17 @@ exports.testBasic = function(options) {
       }
     }
   ]);
 };
 
 exports.testDocument = function(options) {
   return helpers.audit(options, [
     {
-      skipRemainingIf: jsTestAllowed,
+      skipRemainingIf: jsTestDisallowed,
       setup:    '{ docu',
       check: {
         input:  '{ docu',
         hints:        'ment',
         markup: 'VVIIII',
         cursor: 6,
         current: 'javascript',
         status: 'ERROR',
@@ -310,17 +292,18 @@ exports.testDocument = function(options)
         current: 'javascript',
         status: 'VALID',
         predictions: [ ],
         unassigned: [ ],
         args: {
           command: { name: '{' },
           javascript: {
             value: 'document.title',
-            arg: '{ document.title ',
+            // arg: '{ document.title ',
+            // Node/JSDom gets this wrong and omits the trailing space. Why?
             status: 'VALID',
             message: ''
           }
         }
       }
     },
     {
       setup:    '{ document.title',
@@ -343,24 +326,19 @@ exports.testDocument = function(options)
           }
         }
       }
     }
   ]);
 };
 
 exports.testDonteval = function(options) {
-  if (!options.isNoDom) {
-    // nodom causes an eval here, maybe that's node/v8?
-    assert.ok('donteval' in options.window, 'donteval exists');
-  }
-
   return helpers.audit(options, [
     {
-      skipRemainingIf: jsTestAllowed,
+      skipRemainingIf: true, // Commented out until we fix non-enumerable props
       setup:    '{ don',
       check: {
         input:  '{ don',
         hints:       'teval',
         markup: 'VVIII',
         cursor: 5,
         current: 'javascript',
         status: 'ERROR',
@@ -471,17 +449,17 @@ exports.testDonteval = function(options)
       }
     }
   ]);
 };
 
 exports.testExec = function(options) {
   return helpers.audit(options, [
     {
-      skipRemainingIf: jsTestAllowed,
+      skipRemainingIf: jsTestDisallowed,
       setup:    '{ 1+1',
       check: {
         input:  '{ 1+1',
         hints:       '',
         markup: 'VVVVV',
         cursor: 5,
         current: 'javascript',
         status: 'VALID',
--- a/browser/devtools/commandline/test/browser_gcli_keyboard1.js
+++ b/browser/devtools/commandline/test/browser_gcli_keyboard1.js
@@ -10,56 +10,29 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testKeyboard1.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_keyboard1.js");
 }
 
-// <INJECTED SOURCE:END>
-
 var javascript = require('gcli/types/javascript');
 // var helpers = require('./helpers');
 
-var tempWindow;
-
-exports.setup = function(options) {
-  tempWindow = javascript.getGlobalObject();
-  javascript.setGlobalObject(options.window);
-};
-
-exports.shutdown = function(options) {
-  javascript.setGlobalObject(tempWindow);
-  tempWindow = undefined;
-};
-
 exports.testSimple = function(options) {
   return helpers.audit(options, [
     {
       setup: 'tsela<TAB>',
       check: { input: 'tselarr ', cursor: 8 }
     },
     {
       setup: 'tsn di<TAB>',
@@ -70,35 +43,30 @@ exports.testSimple = function(options) {
       check: { input: 'tsg aaa ', cursor: 8 }
     }
   ]);
 };
 
 exports.testScript = function(options) {
   return helpers.audit(options, [
     {
-      skipIf: function commandJsMissing() {
-        return options.requisition.system.commands.get('{') == null;
-      },
+      skipRemainingIf: options.isRemote ||
+              options.requisition.system.commands.get('{') == null,
       setup: '{ wind<TAB>',
       check: { input: '{ window' }
     },
     {
-      skipIf: function commandJsMissing() {
-        return options.requisition.system.commands.get('{') == null;
-      },
       setup: '{ window.docum<TAB>',
       check: { input: '{ window.document' }
     }
   ]);
 };
 
 exports.testJsdom = function(options) {
   return helpers.audit(options, [
     {
-      skipIf: function jsDomOrCommandJsMissing() {
-        return options.requisition.system.commands.get('{') == null;
-      },
+      skipIf: options.isRemote ||
+              options.requisition.system.commands.get('{') == null,
       setup: '{ window.document.titl<TAB>',
       check: { input: '{ window.document.title ' }
     }
   ]);
 };
--- a/browser/devtools/commandline/test/browser_gcli_keyboard2.js
+++ b/browser/devtools/commandline/test/browser_gcli_keyboard2.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testKeyboard2.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_keyboard2.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testIncr = function(options) {
   return helpers.audit(options, [
     /*
     // We currently refuse to increment/decrement things with a non-valid
     // status which makes sense for many cases, and is a decent default.
     // However in theory we could do better, these tests are there for then
--- a/browser/devtools/commandline/test/browser_gcli_keyboard3.js
+++ b/browser/devtools/commandline/test/browser_gcli_keyboard3.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testKeyboard3.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_keyboard3.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testDecr = function(options) {
   return helpers.audit(options, [
     /*
     // See notes at top of testIncr in testKeyboard2.js
     {
       setup: 'tsu -70<DOWN>',
--- a/browser/devtools/commandline/test/browser_gcli_keyboard4.js
+++ b/browser/devtools/commandline/test/browser_gcli_keyboard4.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testKeyboard4.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_keyboard4.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testIncrFloat = function(options) {
   return helpers.audit(options, [
     /*
     // See notes at top of testIncr
     {
       setup: 'tsf -70<UP>',
--- a/browser/devtools/commandline/test/browser_gcli_keyboard5.js
+++ b/browser/devtools/commandline/test/browser_gcli_keyboard5.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testKeyboard5.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_keyboard5.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testCompleteDown = function(options) {
   return helpers.audit(options, [
     {
       setup: 'tsn e<DOWN><DOWN><DOWN><DOWN><DOWN><TAB>',
       check: { input: 'tsn exte ' }
     },
--- a/browser/devtools/commandline/test/browser_gcli_keyboard6.js
+++ b/browser/devtools/commandline/test/browser_gcli_keyboard6.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testKeyboard6.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_keyboard6.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testCompleteUp = function(options) {
   return helpers.audit(options, [
     {
       setup: 'tsn e<UP><TAB>',
       check: { input: 'tsn extend ' }
     },
--- a/browser/devtools/commandline/test/browser_gcli_menu.js
+++ b/browser/devtools/commandline/test/browser_gcli_menu.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testMenu.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_menu.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testOptions = function(options) {
   return helpers.audit(options, [
     {
       setup:    'tslong',
       check: {
         input:  'tslong',
--- a/browser/devtools/commandline/test/browser_gcli_node.js
+++ b/browser/devtools/commandline/test/browser_gcli_node.js
@@ -10,59 +10,32 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testNode.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_node.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
-var nodetype = require('gcli/types/node');
-
-exports.setup = function(options) {
-  if (options.window) {
-    nodetype.setDocument(options.window.document);
-  }
-};
-
-exports.shutdown = function(options) {
-  nodetype.unsetDocument();
-};
 
 exports.testNode = function(options) {
   return helpers.audit(options, [
     {
-      skipRemainingIf: options.isNoDom,
       setup:    'tse ',
       check: {
         input:  'tse ',
         hints:      '<node> [options]',
         markup: 'VVVV',
         cursor: 4,
         current: 'node',
         status: 'ERROR',
@@ -160,21 +133,18 @@ exports.testNode = function(options) {
           nodes2: { status: 'VALID' }
         }
       }
     }
   ]);
 };
 
 exports.testNodeDom = function(options) {
-  var requisition = options.requisition;
-
   return helpers.audit(options, [
     {
-      skipRemainingIf: options.isNoDom,
       setup:    'tse :root',
       check: {
         input:  'tse :root',
         hints:           ' [options]',
         markup: 'VVVVVVVVV',
         cursor: 9,
         current: 'node',
         status: 'VALID',
@@ -197,20 +167,22 @@ exports.testNodeDom = function(options) 
         status: 'VALID',
         args: {
           command: { name: 'tse' },
           node: { arg: ' :root ', status: 'VALID' },
           nodes: { status: 'VALID' },
           nodes2: { status: 'VALID' }
         }
       },
-      post: function() {
-        assert.is(requisition.getAssignment('node').value.tagName,
-                  'HTML',
-                  'root id');
+      exec: {
+      },
+      post: function(output) {
+        if (!options.isRemote) {
+          assert.is(output.args.node.tagName, 'HTML', ':root tagName');
+        }
       }
     },
     {
       setup:    'tse #gcli-nomatch',
       check: {
         input:  'tse #gcli-nomatch',
         hints:                   ' [options]',
         markup: 'VVVVIIIIIIIIIIIII',
@@ -229,39 +201,44 @@ exports.testNodeDom = function(options) 
           nodes2: { status: 'VALID' }
         }
       }
     }
   ]);
 };
 
 exports.testNodes = function(options) {
-  var requisition = options.requisition;
-
   return helpers.audit(options, [
     {
-      skipRemainingIf: options.isNoDom,
       setup:    'tse :root --nodes *',
       check: {
         input:  'tse :root --nodes *',
         hints:                       ' [options]',
         markup: 'VVVVVVVVVVVVVVVVVVV',
         current: 'nodes',
         status: 'VALID',
         args: {
           command: { name: 'tse' },
           node: { arg: ' :root', status: 'VALID' },
           nodes: { arg: ' --nodes *', status: 'VALID' },
           nodes2: { status: 'VALID' }
         }
       },
-      post: function() {
-        assert.is(requisition.getAssignment('node').value.tagName,
-                  'HTML',
-                  '#gcli-input id');
+      exec: {
+      },
+      post: function(output) {
+        if (!options.isRemote) {
+          assert.is(output.args.node.tagName, 'HTML', ':root tagName');
+          assert.ok(output.args.nodes.length > 3, 'nodes length');
+          assert.is(output.args.nodes2.length, 0, 'nodes2 length');
+        }
+
+        assert.is(output.data.args.node, ':root', 'node data');
+        assert.is(output.data.args.nodes, '*', 'nodes data');
+        assert.is(output.data.args.nodes2, 'Error', 'nodes2 data');
       }
     },
     {
       setup:    'tse :root --nodes2 div',
       check: {
         input:  'tse :root --nodes2 div',
         hints:                       ' [options]',
         markup: 'VVVVVVVVVVVVVVVVVVVVVV',
@@ -270,20 +247,28 @@ exports.testNodes = function(options) {
         status: 'VALID',
         args: {
           command: { name: 'tse' },
           node: { arg: ' :root', status: 'VALID' },
           nodes: { status: 'VALID' },
           nodes2: { arg: ' --nodes2 div', status: 'VALID' }
         }
       },
-      post: function() {
-        assert.is(requisition.getAssignment('node').value.tagName,
-                  'HTML',
-                  'root id');
+      exec: {
+      },
+      post: function(output) {
+        if (!options.isRemote) {
+          assert.is(output.args.node.tagName, 'HTML', ':root tagName');
+          assert.is(output.args.nodes.length, 0, 'nodes length');
+          assert.is(output.args.nodes2.item(0).tagName, 'DIV', 'div tagName');
+        }
+
+        assert.is(output.data.args.node, ':root', 'node data');
+        assert.is(output.data.args.nodes, 'Error', 'nodes data');
+        assert.is(output.data.args.nodes2, 'div', 'nodes2 data');
       }
     },
     {
       setup:    'tse --nodes ffff',
       check: {
         input:  'tse --nodes ffff',
         hints:                  ' <node> [options]',
         markup: 'VVVVIIIIIIIVIIII',
@@ -300,23 +285,16 @@ exports.testNodes = function(options) {
           nodes: {
             value: undefined,
             arg: ' --nodes ffff',
             status: 'INCOMPLETE',
             message: 'No matches'
           },
           nodes2: { arg: '', status: 'VALID', message: '' }
         }
-      },
-      post: function() {
-        /*
-        assert.is(requisition.getAssignment('nodes2').value.constructor.name,
-                  'NodeList',
-                  '#gcli-input id');
-        */
       }
     },
     {
       setup:    'tse --nodes2 ffff',
       check: {
         input:  'tse --nodes2 ffff',
         hints:                   ' <node> [options]',
         markup: 'VVVVVVVVVVVVVVVVV',
@@ -328,22 +306,12 @@ exports.testNodes = function(options) {
           node: {
             value: undefined,
             arg: '',
             status: 'INCOMPLETE'
           },
           nodes: { arg: '', status: 'VALID', message: '' },
           nodes2: { arg: ' --nodes2 ffff', status: 'VALID', message: '' }
         }
-      },
-      post: function() {
-        /*
-        assert.is(requisition.getAssignment('nodes').value.constructor.name,
-                  'NodeList',
-                  '#gcli-input id');
-        assert.is(requisition.getAssignment('nodes2').value.constructor.name,
-                  'NodeList',
-                  '#gcli-input id');
-        */
       }
     },
   ]);
 };
--- a/browser/devtools/commandline/test/browser_gcli_pref1.js
+++ b/browser/devtools/commandline/test/browser_gcli_pref1.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testPref1.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_pref1.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testPrefShowStatus = function(options) {
   return helpers.audit(options, [
     {
       skipRemainingIf: options.requisition.system.commands.get('pref') == null,
       setup:    'pref s',
       check: {
@@ -62,17 +47,17 @@ exports.testPrefShowStatus = function(op
         markup: 'VVVVVVVVV',
         status: 'ERROR'
       }
     },
     {
       setup:    'pref show ',
       check: {
         typed:  'pref show ',
-        hints:            'allowSet',
+        hints:            'eagerHelper',
         markup: 'VVVVVVVVVV',
         status: 'ERROR'
       }
     },
     {
       setup:    'pref show tempTBo',
       check: {
         typed:  'pref show tempTBo',
@@ -139,31 +124,32 @@ exports.testPrefSetStatus = function(opt
         markup: 'IIIIVIII',
         status: 'ERROR'
       }
     },
     {
       setup:    'pref set ',
       check: {
         typed:  'pref set ',
-        hints:           'allowSet <value>',
+        hints:           'eagerHelper <value>',
         markup: 'VVVVVVVVV',
         status: 'ERROR'
       }
     },
     {
       setup:    'pref set tempTBo',
       check: {
         typed:  'pref set tempTBo',
         hints:                  'ol <value>',
         markup: 'VVVVVVVVVIIIIIII',
         status: 'ERROR'
       }
     },
     {
+      skipIf: options.isRemote,
       setup:    'pref set tempTBool 4',
       check: {
         typed:  'pref set tempTBool 4',
         markup: 'VVVVVVVVVVVVVVVVVVVE',
         status: 'ERROR',
         hints: ''
       }
     },
--- a/browser/devtools/commandline/test/browser_gcli_pref2.js
+++ b/browser/devtools/commandline/test/browser_gcli_pref2.js
@@ -10,75 +10,55 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testPref2.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_pref2.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 var mockSettings = require('./mockSettings');
 
 exports.testPrefExec = function(options) {
   if (options.requisition.system.commands.get('pref') == null) {
     assert.log('Skipping test; missing pref command.');
     return;
   }
 
   if (options.isRemote) {
     assert.log('Skipping test which assumes local settings.');
     return;
   }
 
-  var allowSet = settings.getSetting('allowSet');
-  var initialAllowSet = allowSet.value;
-  allowSet.value = false;
-
   assert.is(mockSettings.tempNumber.value, 42, 'set to 42');
 
   return helpers.audit(options, [
     {
       // Delegated remote types can't transfer value types so we only test for
       // the value of 'value' when we're local
       skipIf: options.isRemote,
       setup: 'pref set tempNumber 4',
       check: {
         setting: { value: mockSettings.tempNumber },
         args: { value: { value: 4 } }
       }
     },
     {
-      skipRemainingIf: options.isNoDom,
       setup:    'pref set tempNumber 4',
       check: {
         input:  'pref set tempNumber 4',
         hints:                       '',
         markup: 'VVVVVVVVVVVVVVVVVVVVV',
         cursor: 21,
         current: 'value',
         status: 'VALID',
@@ -94,26 +74,16 @@ exports.testPrefExec = function(options)
           value: {
             arg: ' 4',
             status: 'VALID',
             message: ''
           }
         }
       },
       exec: {
-        output: [ /void your warranty/, /I promise/ ]
-      },
-      post: function() {
-        assert.is(mockSettings.tempNumber.value, 42, 'still set to 42');
-        allowSet.value = true;
-      }
-    },
-    {
-      setup:    'pref set tempNumber 4',
-      exec: {
         output: ''
       },
       post: function() {
         assert.is(mockSettings.tempNumber.value, 4, 'set to 4');
       }
     },
     {
       setup:    'pref reset tempNumber',
@@ -123,18 +93,16 @@ exports.testPrefExec = function(options)
           setting: { value: mockSettings.tempNumber }
         }
       },
       exec: {
         output: ''
       },
       post: function() {
         assert.is(mockSettings.tempNumber.value, 42, 'reset to 42');
-
-        allowSet.value = initialAllowSet;
       }
     },
     {
       skipRemainingIf: function commandPrefListMissing() {
         return options.requisition.system.commands.get('pref list') == null;
       },
       setup:    'pref list tempNum',
       check: {
--- a/browser/devtools/commandline/test/browser_gcli_remotews.js
+++ b/browser/devtools/commandline/test/browser_gcli_remotews.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testRemoteWs.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_remotews.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 
 // testRemoteWs and testRemoteXhr are virtually identical.
 // Changes made here should be made there too.
 // They are kept separate to save adding complexity to the test system and so
 // to help us select the test that are available in different environments
 
@@ -78,18 +63,18 @@ exports.testRemoteWebsocket = function(o
         error: false
       }
     },
     {
       setup: 'disconnect remote',
       check: {
         args: {
           prefix: {
-            value: function(connection) {
-              assert.is(connection.prefix, 'remote', 'disconnecting remote');
+            value: function(front) {
+              assert.is(front.prefix, 'remote', 'disconnecting remote');
             }
           }
         }
       },
       exec: {
         output: /^Removed [0-9]* commands.$/,
         type: 'string',
         error: false
@@ -107,18 +92,18 @@ exports.testRemoteWebsocket = function(o
         error: false
       }
     },
     {
       setup: 'disconnect remote',
       check: {
         args: {
           prefix: {
-            value: function(connection) {
-              assert.is(connection.prefix, 'remote', 'disconnecting remote');
+            value: function(front) {
+              assert.is(front.prefix, 'remote', 'disconnecting remote');
             }
           }
         }
       },
       exec: {
         output: /^Removed [0-9]* commands.$/,
         type: 'string',
         error: false
@@ -461,18 +446,18 @@ exports.testRemoteWebsocket = function(o
         input:  'disconnect remote',
         hints:                   '',
         markup: 'VVVVVVVVVVVVVVVVV',
         status: 'VALID',
         message: '',
         unassigned: [ ],
         args: {
           prefix: {
-            value: function(connection) {
-              assert.is(connection.prefix, 'remote', 'disconnecting remote');
+            value: function(front) {
+              assert.is(front.prefix, 'remote', 'disconnecting remote');
             },
             arg: ' remote',
             status: 'VALID',
             message: ''
           }
         }
       },
       exec: {
--- a/browser/devtools/commandline/test/browser_gcli_remotexhr.js
+++ b/browser/devtools/commandline/test/browser_gcli_remotexhr.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testRemoteXhr.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_remotexhr.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 
 // testRemoteWs and testRemoteXhr are virtually identical.
 // Changes made here should be made there too.
 // They are kept separate to save adding complexity to the test system and so
 // to help us select the test that are available in different environments
 
@@ -78,18 +63,18 @@ exports.testRemoteXhr = function(options
         error: false
       }
     },
     {
       setup: 'disconnect remote',
       check: {
         args: {
           prefix: {
-            value: function(connection) {
-              assert.is(connection.prefix, 'remote', 'disconnecting remote');
+            value: function(front) {
+              assert.is(front.prefix, 'remote', 'disconnecting remote');
             }
           }
         }
       },
       exec: {
         output: /^Removed [0-9]* commands.$/,
         type: 'string',
         error: false
@@ -107,18 +92,18 @@ exports.testRemoteXhr = function(options
         error: false
       }
     },
     {
       setup: 'disconnect remote',
       check: {
         args: {
           prefix: {
-            value: function(connection) {
-              assert.is(connection.prefix, 'remote', 'disconnecting remote');
+            value: function(front) {
+              assert.is(front.prefix, 'remote', 'disconnecting remote');
             }
           }
         }
       },
       exec: {
         output: /^Removed [0-9]* commands.$/,
         type: 'string',
         error: false
@@ -461,18 +446,18 @@ exports.testRemoteXhr = function(options
         input:  'disconnect remote',
         hints:                   '',
         markup: 'VVVVVVVVVVVVVVVVV',
         status: 'VALID',
         message: '',
         unassigned: [ ],
         args: {
           prefix: {
-            value: function(connection) {
-              assert.is(connection.prefix, 'remote', 'disconnecting remote');
+            value: function(front) {
+              assert.is(front.prefix, 'remote', 'disconnecting remote');
             },
             arg: ' remote',
             status: 'VALID',
             message: ''
           }
         }
       },
       exec: {
--- a/browser/devtools/commandline/test/browser_gcli_resource.js
+++ b/browser/devtools/commandline/test/browser_gcli_resource.js
@@ -10,156 +10,142 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testResource.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_resource.js");
 }
 
-// <INJECTED SOURCE:END>
-
+// var helpers = require('./helpers');
 // var assert = require('../testharness/assert');
 
 var Promise = require('gcli/util/promise').Promise;
 var util = require('gcli/util/util');
 var resource = require('gcli/types/resource');
 var Status = require('gcli/types/types').Status;
 
-
-var tempDocument;
-
-exports.setup = function(options) {
-  tempDocument = resource.getDocument();
-  if (options.window) {
-    resource.setDocument(options.window.document);
-  }
-};
-
-exports.shutdown = function(options) {
-  resource.setDocument(tempDocument);
-  tempDocument = undefined;
+exports.testCommand = function(options) {
+  return helpers.audit(options, [
+    {
+      setup:    'tsres ',
+      check: {
+        predictionsContains: [ 'inline-css' ],
+      }
+    }
+  ]);
 };
 
 exports.testAllPredictions1 = function(options) {
-  if (options.isFirefox || options.isNoDom) {
-    assert.log('Skipping checks due to firefox document.stylsheets support.');
+  if (options.isRemote) {
+    assert.log('Can\'t directly test remote types locally.');
     return;
   }
 
+  var context = options.requisition.conversionContext;
   var resource = options.requisition.system.types.createType('resource');
-  return resource.getLookup().then(function(opts) {
+  return resource.getLookup(context).then(function(opts) {
     assert.ok(opts.length > 1, 'have all resources');
 
     return util.promiseEach(opts, function(prediction) {
-      return checkPrediction(resource, prediction);
+      return checkPrediction(resource, prediction, context);
     });
   });
 };
 
 exports.testScriptPredictions = function(options) {
-  if (options.isFirefox || options.isNoDom) {
-    assert.log('Skipping checks due to firefox document.stylsheets support.');
+  if (options.isRemote || options.isNode) {
+    assert.log('Can\'t directly test remote types locally.');
     return;
   }
 
+  var context = options.requisition.conversionContext;
   var types = options.requisition.system.types;
   var resource = types.createType({ name: 'resource', include: 'text/javascript' });
-  return resource.getLookup().then(function(opts) {
+  return resource.getLookup(context).then(function(opts) {
     assert.ok(opts.length > 1, 'have js resources');
 
     return util.promiseEach(opts, function(prediction) {
-      return checkPrediction(resource, prediction);
+      return checkPrediction(resource, prediction, context);
     });
   });
 };
 
 exports.testStylePredictions = function(options) {
-  if (options.isFirefox || options.isNoDom) {
-    assert.log('Skipping checks due to firefox document.stylsheets support.');
+  if (options.isRemote) {
+    assert.log('Can\'t directly test remote types locally.');
     return;
   }
 
+  var context = options.requisition.conversionContext;
   var types = options.requisition.system.types;
   var resource = types.createType({ name: 'resource', include: 'text/css' });
-  return resource.getLookup().then(function(opts) {
+  return resource.getLookup(context).then(function(opts) {
     assert.ok(opts.length >= 1, 'have css resources');
 
     return util.promiseEach(opts, function(prediction) {
-      return checkPrediction(resource, prediction);
+      return checkPrediction(resource, prediction, context);
     });
   });
 };
 
 exports.testAllPredictions2 = function(options) {
-  if (options.isNoDom) {
-    assert.log('Skipping checks due to nodom document.stylsheets support.');
+  if (options.isRemote) {
+    assert.log('Can\'t directly test remote types locally.');
     return;
   }
+
+  var context = options.requisition.conversionContext;
   var types = options.requisition.system.types;
 
   var scriptRes = types.createType({ name: 'resource', include: 'text/javascript' });
-  return scriptRes.getLookup().then(function(scriptOptions) {
+  return scriptRes.getLookup(context).then(function(scriptOptions) {
     var styleRes = types.createType({ name: 'resource', include: 'text/css' });
-    return styleRes.getLookup().then(function(styleOptions) {
+    return styleRes.getLookup(context).then(function(styleOptions) {
       var allRes = types.createType({ name: 'resource' });
-      return allRes.getLookup().then(function(allOptions) {
+      return allRes.getLookup(context).then(function(allOptions) {
         assert.is(scriptOptions.length + styleOptions.length,
                   allOptions.length,
                   'split');
       });
     });
   });
 };
 
 exports.testAllPredictions3 = function(options) {
-  if (options.isNoDom) {
-    assert.log('Skipping checks due to nodom document.stylsheets support.');
+  if (options.isRemote) {
+    assert.log('Can\'t directly test remote types locally.');
     return;
   }
 
+  var context = options.requisition.conversionContext;
   var types = options.requisition.system.types;
   var res1 = types.createType({ name: 'resource' });
-  return res1.getLookup().then(function(options1) {
+  return res1.getLookup(context).then(function(options1) {
     var res2 = types.createType('resource');
-    return res2.getLookup().then(function(options2) {
+    return res2.getLookup(context).then(function(options2) {
       assert.is(options1.length, options2.length, 'type spec');
     });
   });
 };
 
-function checkPrediction(res, prediction) {
+function checkPrediction(res, prediction, context) {
   var name = prediction.name;
   var value = prediction.value;
 
-  // resources don't need context so cheat and pass in null
-  var context = null;
   return res.parseString(name, context).then(function(conversion) {
     assert.is(conversion.getStatus(), Status.VALID, 'status VALID for ' + name);
     assert.is(conversion.value, value, 'value for ' + name);
 
     assert.is(typeof value.loadContents, 'function', 'resource for ' + name);
     assert.is(typeof value.element, 'object', 'resource for ' + name);
 
     return Promise.resolve(res.stringify(value, context)).then(function(strung) {
--- a/browser/devtools/commandline/test/browser_gcli_short.js
+++ b/browser/devtools/commandline/test/browser_gcli_short.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testShort.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_short.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testBasic = function(options) {
   return helpers.audit(options, [
     {
       setup:    'tshidden -v',
       check: {
         input:  'tshidden -v',
--- a/browser/devtools/commandline/test/browser_gcli_spell.js
+++ b/browser/devtools/commandline/test/browser_gcli_spell.js
@@ -9,41 +9,26 @@
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
  * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
  * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testSpell.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_spell.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 var spell = require('gcli/util/spell');
 
 exports.testSpellerSimple = function(options) {
   var alternatives = [
     'window', 'document', 'InstallTrigger', 'requirejs', 'require','define',
     'console', 'location', 'constructor', 'getInterface', 'external', 'sidebar'
   ];
--- a/browser/devtools/commandline/test/browser_gcli_split.js
+++ b/browser/devtools/commandline/test/browser_gcli_split.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testSplit.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_split.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 
 var cli = require('gcli/cli');
 
 exports.testSplitSimple = function(options) {
   var args = cli.tokenize('s');
   options.requisition._split(args);
   assert.is(args.length, 0);
--- a/browser/devtools/commandline/test/browser_gcli_string.js
+++ b/browser/devtools/commandline/test/browser_gcli_string.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testString.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_string.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var helpers = require('./helpers');
 
 exports.testNewLine = function(options) {
   return helpers.audit(options, [
     {
       setup:    'echo a\\nb',
       check: {
         input:  'echo a\\nb',
--- a/browser/devtools/commandline/test/browser_gcli_tokenize.js
+++ b/browser/devtools/commandline/test/browser_gcli_tokenize.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testTokenize.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_tokenize.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 var cli = require('gcli/cli');
 
 exports.testBlanks = function(options) {
   var args;
 
   args = cli.tokenize('');
   assert.is(args.length, 1);
--- a/browser/devtools/commandline/test/browser_gcli_tooltip.js
+++ b/browser/devtools/commandline/test/browser_gcli_tooltip.js
@@ -10,50 +10,30 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testTooltip.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_tooltip.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 
 exports.testActivate = function(options) {
-  if (!options.display) {
-    assert.log('No display. Skipping activate tests');
-    return;
-  }
-
   return helpers.audit(options, [
     {
       setup:    ' ',
       check: {
         input:  ' ',
         hints:   '',
         markup: 'V',
         cursor: 1,
--- a/browser/devtools/commandline/test/browser_gcli_types.js
+++ b/browser/devtools/commandline/test/browser_gcli_types.js
@@ -10,59 +10,35 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testTypes.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_types.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 var util = require('gcli/util/util');
 var Promise = require('gcli/util/promise').Promise;
-var nodetype = require('gcli/types/node');
 
-exports.setup = function(options) {
-  if (options.window) {
-    nodetype.setDocument(options.window.document);
-  }
-};
-
-exports.shutdown = function(options) {
-  nodetype.unsetDocument();
-};
-
-function forEachType(options, typeSpec, callback) {
+function forEachType(options, templateTypeSpec, callback) {
   var types = options.requisition.system.types;
   return util.promiseEach(types.getTypeNames(), function(name) {
+    var typeSpec = {};
+    util.copyProperties(templateTypeSpec, typeSpec);
     typeSpec.name = name;
     typeSpec.requisition = options.requisition;
 
     // Provide some basic defaults to help selection/delegate/array work
     if (name === 'selection') {
       typeSpec.data = [ 'a', 'b' ];
     }
     else if (name === 'delegate') {
@@ -74,39 +50,29 @@ function forEachType(options, typeSpec, 
       typeSpec.subtype = 'string';
     }
     else if (name === 'remote') {
       return;
     }
     else if (name === 'union') {
       typeSpec.alternatives = [{ name: 'string' }];
     }
+    else if (options.isRemote) {
+      if (name === 'node' || name === 'nodelist') {
+        return;
+      }
+    }
 
     var type = types.createType(typeSpec);
     var reply = callback(type);
-    return Promise.resolve(reply).then(function(value) {
-      // Clean up
-      delete typeSpec.name;
-      delete typeSpec.requisition;
-      delete typeSpec.data;
-      delete typeSpec.delegateType;
-      delete typeSpec.subtype;
-      delete typeSpec.alternatives;
-
-      return value;
-    });
+    return Promise.resolve(reply);
   });
 }
 
 exports.testDefault = function(options) {
-  if (options.isNoDom) {
-    assert.log('Skipping tests due to issues with resource type.');
-    return;
-  }
-
   return forEachType(options, {}, function(type) {
     var context = options.requisition.executionContext;
     var blank = type.getBlank(context).value;
 
     // boolean and array types are exempt from needing undefined blank values
     if (type.name === 'boolean') {
       assert.is(blank, false, 'blank boolean is false');
     }
--- a/browser/devtools/commandline/test/browser_gcli_union.js
+++ b/browser/devtools/commandline/test/browser_gcli_union.js
@@ -10,41 +10,26 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testUnion.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_union.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 
 exports.testDefault = function(options) {
   return helpers.audit(options, [
     {
       setup:    'unionc1',
       check: {
@@ -121,17 +106,17 @@ exports.testDefault = function(options) 
       post: function(output, text) {
         var data = output.data.first;
         assert.is(Object.keys(data).length, 2, 'union1 Object.keys');
         assert.is(data.type, 'selection', 'union1 val type');
         assert.is(data.selection, 1, 'union1 val selection');
       }
     },
     {
-      skipIf: options.isPhantomjs, // Phantom goes weird with predictions
+      skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong
       setup:    'unionc1 5',
       check: {
         input:  'unionc1 5',
         markup: 'VVVVVVVVV',
         hints:           ' -> two',
         predictions: [ 'two' ],
         status: 'VALID',
         args: {
@@ -155,17 +140,17 @@ exports.testDefault = function(options) 
       post: function(output, text) {
         var data = output.data.first;
         assert.is(Object.keys(data).length, 2, 'union5 Object.keys');
         assert.is(data.type, 'number', 'union5 val type');
         assert.is(data.number, 5, 'union5 val number');
       }
     },
     {
-      skipRemainingIf: options.isPhantomjs,
+      skipIf: options.isPhantomjs, // PhantomJS URL type is broken
       setup:    'unionc2 on',
       check: {
         input:  'unionc2 on',
         hints:            'e',
         markup: 'VVVVVVVVII',
         current: 'first',
         status: 'ERROR',
         predictionsContains: [
--- a/browser/devtools/commandline/test/browser_gcli_url.js
+++ b/browser/devtools/commandline/test/browser_gcli_url.js
@@ -10,48 +10,33 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testUrl.js</p>";
+const exports = {};
 
 function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
+  helpers.runTestModule(exports, "browser_gcli_url.js");
 }
 
-// <INJECTED SOURCE:END>
-
 // var assert = require('../testharness/assert');
 // var helpers = require('./helpers');
 
 exports.testDefault = function(options) {
   return helpers.audit(options, [
     {
-      skipRemainingIf: options.isPhantomjs,
+      skipRemainingIf: options.isPhantomjs, // PhantomJS URL type is broken
       setup:    'urlc',
       check: {
         input:  'urlc',
         markup: 'VVVV',
         hints:        ' <url>',
         status: 'ERROR',
         args: {
           url: {
--- a/browser/devtools/commandline/test/helpers.js
+++ b/browser/devtools/commandline/test/helpers.js
@@ -13,84 +13,85 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
 
 // A copy of this code exists in firefox mochitests. They should be kept
 // in sync. Hence the exports synonym for non AMD contexts.
-var { helpers, gcli, assert } = (function() {
+var { helpers, assert } = (function() {
 
 var helpers = {};
 
 var TargetFactory = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.TargetFactory;
 var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 
 var assert = { ok: ok, is: is, log: info };
 var util = require('gcli/util/util');
 var Promise = require('gcli/util/promise').Promise;
 var cli = require('gcli/cli');
 var KeyEvent = require('gcli/util/util').KeyEvent;
-var gcli = require('gcli/index');
+
+const { GcliFront } = require("devtools/server/actors/gcli");
 
 /**
  * See notes in helpers.checkOptions()
  */
-var createFFDisplayAutomator = function(display) {
+var createDeveloperToolbarAutomator = function(toolbar) {
   var automator = {
     setInput: function(typed) {
-      return display.inputter.setInput(typed);
+      return toolbar.inputter.setInput(typed);
     },
 
     setCursor: function(cursor) {
-      return display.inputter.setCursor(cursor);
+      return toolbar.inputter.setCursor(cursor);
     },
 
     focus: function() {
-      return display.inputter.focus();
+      return toolbar.inputter.focus();
     },
 
     fakeKey: function(keyCode) {
       var fakeEvent = {
         keyCode: keyCode,
         preventDefault: function() { },
         timeStamp: new Date().getTime()
       };
 
-      display.inputter.onKeyDown(fakeEvent);
+      toolbar.inputter.onKeyDown(fakeEvent);
 
       if (keyCode === KeyEvent.DOM_VK_BACK_SPACE) {
-        var input = display.inputter.element;
+        var input = toolbar.inputter.element;
         input.value = input.value.slice(0, -1);
       }
 
-      return display.inputter.handleKeyUp(fakeEvent);
+      return toolbar.inputter.handleKeyUp(fakeEvent);
     },
 
     getInputState: function() {
-      return display.inputter.getInputState();
+      return toolbar.inputter.getInputState();
     },
 
     getCompleterTemplateData: function() {
-      return display.completer._getCompleterTemplateData();
+      return toolbar.completer._getCompleterTemplateData();
     },
 
     getErrorMessage: function() {
-      return display.tooltip.errorEle.textContent;
+      return toolbar.tooltip.errorEle.textContent;
     }
   };
 
   Object.defineProperty(automator, 'focusManager', {
-    get: function() { return display.focusManager; },
+    get: function() { return toolbar.focusManager; },
     enumerable: true
   });
 
   Object.defineProperty(automator, 'field', {
-    get: function() { return display.tooltip.field; },
+    get: function() { return toolbar.tooltip.field; },
     enumerable: true
   });
 
   return automator;
 };
 
 /**
  * Warning: For use with Firefox Mochitests only.
@@ -218,19 +219,19 @@ helpers.closeTab = function(options) {
  * @return A promise which resolves to the options object when the 'load' event
  * happens on the new tab
  */
 helpers.openToolbar = function(options) {
   options = options || {};
   options.chromeWindow = options.chromeWindow || window;
 
   return options.chromeWindow.DeveloperToolbar.show(true).then(function() {
-    var display = options.chromeWindow.DeveloperToolbar.display;
-    options.automator = createFFDisplayAutomator(display);
-    options.requisition = display.requisition;
+    var toolbar = options.chromeWindow.DeveloperToolbar;
+    options.automator = createDeveloperToolbarAutomator(toolbar);
+    options.requisition = toolbar.requisition;
     return options;
   });
 };
 
 /**
  * Navigate the current tab to a URL
  */
 helpers.navigate = function(url, options) {
@@ -326,27 +327,27 @@ helpers.promiseify = function(functionWi
     });
   };
 };
 
 /**
  * Warning: For use with Firefox Mochitests only.
  *
  * As addTab, but that also opens the developer toolbar. In addition a new
- * 'automator' property is added to the options object with the display from GCLI
- * in the developer toolbar
+ * 'automator' property is added to the options object which uses the
+ * developer toolbar
  */
 helpers.addTabWithToolbar = function(url, callback, options) {
   return helpers.addTab(url, function(innerOptions) {
     var win = innerOptions.chromeWindow;
 
     return win.DeveloperToolbar.show(true).then(function() {
-      var display = win.DeveloperToolbar.display;
-      innerOptions.automator = createFFDisplayAutomator(display);
-      innerOptions.requisition = display.requisition;
+      var toolbar = win.DeveloperToolbar;
+      innerOptions.automator = createDeveloperToolbarAutomator(toolbar);
+      innerOptions.requisition = toolbar.requisition;
 
       var reply = callback.call(null, innerOptions);
 
       return Promise.resolve(reply).then(null, function(error) {
         ok(false, error);
         console.error(error);
       }).then(function() {
         win.DeveloperToolbar.hide().then(function() {
@@ -371,17 +372,17 @@ helpers.addTabWithToolbar = function(url
  */
 helpers.runTests = function(options, tests) {
   var testNames = Object.keys(tests).filter(function(test) {
     return test != "setup" && test != "shutdown";
   });
 
   var recover = function(error) {
     ok(false, error);
-    console.error(error);
+    console.error(error, error.stack);
   };
 
   info("SETUP");
   var setupDone = (tests.setup != null) ?
       Promise.resolve(tests.setup(options)) :
       Promise.resolve();
 
   var testDone = setupDone.then(function() {
@@ -405,16 +406,97 @@ helpers.runTests = function(options, tes
   return testDone.then(function() {
     info("SHUTDOWN");
     return (tests.shutdown != null) ?
         Promise.resolve(tests.shutdown(options)) :
         Promise.resolve();
   }, recover);
 };
 
+const MOCK_COMMANDS_URI = "chrome://mochitests/content/browser/browser/devtools/commandline/test/mockCommands.js";
+
+const defer = function() {
+  const deferred = { };
+  deferred.promise = new Promise(function(resolve, reject) {
+    deferred.resolve = resolve;
+    deferred.reject = reject;
+  });
+  return deferred;
+};
+
+/**
+ * This does several actions associated with running a GCLI test in mochitest
+ * 1. Create a new tab containing basic markup for GCLI tests
+ * 2. Open the developer toolbar
+ * 3. Register the mock commands with the server process
+ * 4. Wait for the proxy commands to be auto-regitstered with the client
+ * 5. Register the mock converters with the client process
+ * 6. Run all the tests
+ * 7. Tear down all the setup
+ */
+helpers.runTestModule = function(exports, name) {
+  return Task.spawn(function*() {
+    const uri = "data:text/html;charset=utf-8," +
+                "<style>div{color:red;}</style>" +
+                "<div id='gcli-root'>" + name + "</div>";
+
+    const options = yield helpers.openTab(uri);
+    options.isRemote = true;
+
+    yield helpers.openToolbar(options);
+
+    const system = options.requisition.system;
+
+    // Register a one time listener with the local set of commands
+    const addedDeferred = defer();
+    const removedDeferred = defer();
+    let state = 'preAdd'; // Then 'postAdd' then 'postRemove'
+
+    system.commands.onCommandsChange.add(function(ev) {
+      if (system.commands.get('tsslow') != null) {
+        if (state === 'preAdd') {
+          addedDeferred.resolve();
+          state = 'postAdd';
+        }
+      }
+      else {
+        if (state === 'postAdd') {
+          removedDeferred.resolve();
+          state = 'postRemove';
+        }
+      }
+    });
+
+    // Send a message to add the commands to the content process
+    const front = yield GcliFront.create(options.target);
+    yield front._testOnly_addItemsByModule(MOCK_COMMANDS_URI);
+
+    // This will cause the local set of commands to be updated with the
+    // command proxies, wait for that to complete.
+    yield addedDeferred.promise;
+
+    // Now we need to add the converters to the local GCLI
+    const converters = mockCommands.items.filter(item => item.item === 'converter');
+    system.addItems(converters);
+
+    // Next run the tests
+    yield helpers.runTests(options, exports);
+
+    // Finally undo the mock commands and converters
+    system.removeItems(converters);
+    const removePromise = system.commands.onCommandsChange.once();
+    yield front._testOnly_removeItemsByModule(MOCK_COMMANDS_URI);
+    yield removedDeferred.promise;
+
+    // And close everything down
+    yield helpers.closeToolbar(options);
+    yield helpers.closeTab(options);
+  }).then(finish, helpers.handleError);
+};
+
 ///////////////////////////////////////////////////////////////////////////////
 
 /**
  * Ensure that the options object is setup correctly
  * options should contain an automator object that looks like this:
  * {
  *   getInputState: function() { ... },
  *   setCursor: function(cursor) { ... },
@@ -754,90 +836,94 @@ helpers._check = function(options, name,
 
   if (checks == null) {
     return Promise.resolve();
   }
 
   var outstanding = [];
   var suffix = name ? ' (for \'' + name + '\')' : '';
 
-  if (!options.isNoDom && 'input' in checks) {
+  if (!options.isNode && 'input' in checks) {
     assert.is(helpers._actual.input(options), checks.input, 'input' + suffix);
   }
 
-  if (!options.isNoDom && 'cursor' in checks) {
+  if (!options.isNode && 'cursor' in checks) {
     assert.is(helpers._actual.cursor(options), checks.cursor, 'cursor' + suffix);
   }
 
-  if (!options.isNoDom && 'current' in checks) {
+  if (!options.isNode && 'current' in checks) {
     assert.is(helpers._actual.current(options), checks.current, 'current' + suffix);
   }
 
   if ('status' in checks) {
     assert.is(helpers._actual.status(options), checks.status, 'status' + suffix);
   }
 
-  if (!options.isNoDom && 'markup' in checks) {
+  if (!options.isNode && 'markup' in checks) {
     assert.is(helpers._actual.markup(options), checks.markup, 'markup' + suffix);
   }
 
-  if (!options.isNoDom && 'hints' in checks) {
+  if (!options.isNode && 'hints' in checks) {
     var hintCheck = function(actualHints) {
       assert.is(actualHints, checks.hints, 'hints' + suffix);
     };
     outstanding.push(helpers._actual.hints(options).then(hintCheck));
   }
 
-  if (!options.isNoDom && 'predictions' in checks) {
+  if (!options.isNode && 'predictions' in checks) {
     var predictionsCheck = function(actualPredictions) {
       helpers.arrayIs(actualPredictions,
                        checks.predictions,
                        'predictions' + suffix);
     };
     outstanding.push(helpers._actual.predictions(options).then(predictionsCheck));
   }
 
-  if (!options.isNoDom && 'predictionsContains' in checks) {
+  if (!options.isNode && 'predictionsContains' in checks) {
     var containsCheck = function(actualPredictions) {
       checks.predictionsContains.forEach(function(prediction) {
         var index = actualPredictions.indexOf(prediction);
         assert.ok(index !== -1,
                   'predictionsContains:' + prediction + suffix);
+        if (index === -1) {
+          log('Actual predictions (' + actualPredictions.length + '): ' +
+              actualPredictions.join(', '));
+        }
       });
     };
     outstanding.push(helpers._actual.predictions(options).then(containsCheck));
   }
 
   if ('unassigned' in checks) {
     helpers.arrayIs(helpers._actual.unassigned(options),
                      checks.unassigned,
                      'unassigned' + suffix);
   }
 
   /* TODO: Fix this
-  if (!options.isNoDom && 'tooltipState' in checks) {
+  if (!options.isNode && 'tooltipState' in checks) {
     assert.is(helpers._actual.tooltipState(options),
               checks.tooltipState,
               'tooltipState' + suffix);
   }
   */
 
-  if (!options.isNoDom && 'outputState' in checks) {
+  if (!options.isNode && 'outputState' in checks) {
     assert.is(helpers._actual.outputState(options),
               checks.outputState,
               'outputState' + suffix);
   }
 
-  if (!options.isNoDom && 'options' in checks) {
+  if (!options.isNode && 'options' in checks) {
     helpers.arrayIs(helpers._actual.options(options),
                      checks.options,
                      'options' + suffix);
   }
 
-  if (!options.isNoDom && 'error' in checks) {
+  if (!options.isNode && 'error' in checks) {
     assert.is(helpers._actual.message(options), checks.error, 'error' + suffix);
   }
 
   if (checks.args != null) {
     Object.keys(checks.args).forEach(function(paramName) {
       var check = checks.args[paramName];
 
       // We allow an 'argument' called 'command' to be the command itself, but
@@ -889,17 +975,17 @@ helpers._check = function(options, name,
       }
 
       if ('status' in check) {
         assert.is(assignment.getStatus().toString(),
                   check.status,
                   'arg.' + paramName + '.status' + suffix);
       }
 
-      if (!options.isNoDom && 'message' in check) {
+      if (!options.isNode && 'message' in check) {
         if (typeof check.message.test === 'function') {
           assert.ok(check.message.test(assignment.message),
                     'arg.' + paramName + '.message' + suffix);
         }
         else {
           assert.is(assignment.message,
                     check.message,
                     'arg.' + paramName + '.message' + suffix);
@@ -947,22 +1033,22 @@ helpers._exec = function(options, name, 
       }
 
       if (!('output' in expected)) {
         return { output: output };
       }
 
       var context = requisition.conversionContext;
       var convertPromise;
-      if (options.isNoDom) {
+      if (options.isNode) {
         convertPromise = output.convert('string', context);
       }
       else {
         convertPromise = output.convert('dom', context).then(function(node) {
-          return node.textContent.trim();
+          return (node == null) ? '' : node.textContent.trim();
         });
       }
 
       return convertPromise.then(function(textOutput) {
         var doTest = function(match, against) {
           // Only log the real textContent if the test fails
           if (against.match(match) != null) {
             assert.ok(true, 'html output for \'' + name + '\' ' +
@@ -1166,19 +1252,18 @@ helpers.audit = function(options, audits
           audit.skipRemainingIf(options) :
           !!audit.skipRemainingIf;
       if (skipRemainingIf) {
         skipReason = audit.skipRemainingIf.name ?
             'due to ' + audit.skipRemainingIf.name :
             '';
         assert.log('Skipped ' + name + ' ' + skipReason);
 
-        // Tests need at least one pass, fail or todo. Let's create a dummy pass
-        // in case there are none.
-        ok(true, "Each test requires at least one pass, fail or todo so here is a pass.");
+        // Tests need at least one pass, fail or todo. Create a dummy pass
+        assert.ok(true, 'Each test requires at least one pass, fail or todo');
 
         return Promise.resolve(undefined);
       }
     }
 
     if (audit.skipIf) {
       var skip = (typeof audit.skipIf === 'function') ?
           audit.skipIf(options) :
@@ -1265,10 +1350,10 @@ function log(message) {
   if (typeof info === 'function') {
     info(message);
   }
   else {
     console.log(message);
   }
 }
 
-return { helpers: helpers, gcli: gcli, assert: assert };
+return { helpers: helpers, assert: assert };
 })();
--- a/browser/devtools/commandline/test/mockCommands.js
+++ b/browser/devtools/commandline/test/mockCommands.js
@@ -10,68 +10,111 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 'use strict';
-// <INJECTED SOURCE:START>
 
 // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
-
-// <INJECTED SOURCE:END>
-
+// PLEASE TALK TO SOMEONE IN DEVELOPER TOOLS BEFORE EDITING IT
 
 var Promise = require('gcli/util/promise').Promise;
-var mockCommands = {};
+
+var mockCommands;
+if (typeof exports !== 'undefined') {
+  // If we're being loaded via require();
+  mockCommands = exports;
+}
+else {
+  // If we're being loaded via loadScript in mochitest
+  mockCommands = {};
+}
 
 // We use an alias for exports here because this module is used in Firefox
 // mochitests where we don't have define/require
 
 /**
  * Registration and de-registration.
  */
 mockCommands.setup = function(requisition) {
   requisition.system.addItems(mockCommands.items);
 };
 
 mockCommands.shutdown = function(requisition) {
   requisition.system.removeItems(mockCommands.items);
 };
 
 function createExec(name) {
-  return function(args, executionContext) {
-    var argsOut = Object.keys(args).map(function(key) {
-      return key + '=' + args[key];
-    }).join(', ');
-    return 'Exec: ' + name + ' ' + argsOut;
+  return function(args, context) {
+    var promises = [];
+
+    Object.keys(args).map(function(argName) {
+      var value = args[argName];
+      var type = this.getParameterByName(argName).type;
+      var promise = Promise.resolve(type.stringify(value, context));
+      promises.push(promise.then(function(str) {
+        return { name: argName, value: str };
+      }.bind(this)));
+    }.bind(this));
+
+    return Promise.all(promises).then(function(data) {
+      var argValues = {};
+      data.forEach(function(entry) { argValues[entry.name] = entry.value; });
+
+      return context.typedData('testCommandOutput', {
+        name: name,
+        args: argValues
+      });
+    }.bind(this));
   };
 }
 
 mockCommands.items = [
   {
     item: 'converter',
-    from: 'json',
-    to: 'string',
-    exec: function(json, context) {
-      return JSON.stringify(json, null, '  ');
+    from: 'testCommandOutput',
+    to: 'dom',
+    exec: function(testCommandOutput, context) {
+      var view = context.createView({
+        data: testCommandOutput,
+        html: '' +
+          '<table>' +
+            '<thead>' +
+              '<tr>' +
+                '<th colspan="3">Exec: ${name}</th>' +
+              '</tr>' +
+            '</thead>' +
+            '<tbody>' +
+              '<tr foreach="key in ${args}">' +
+                '<td> ${key}</td>' +
+                '<td>=</td>' +
+                '<td>${args[key]}</td>' +
+              '</tr>' +
+            '</tbody>' +
+          '</table>',
+        options: {
+          allowEval: true
+        }
+      });
+
+      return view.toDom(context.document);
     }
   },
   {
     item: 'converter',
-    from: 'json',
-    to: 'view',
-    exec: function(json, context) {
-      var html = JSON.stringify(json, null, '&#160;').replace(/\n/g, '<br/>');
-      return {
-        html: '<pre>' + html + '</pre>'
-      };
+    from: 'testCommandOutput',
+    to: 'string',
+    exec: function(testCommandOutput, context) {
+      var argsOut = Object.keys(testCommandOutput.args).map(function(key) {
+        return key + '=' + testCommandOutput.args[key];
+      }).join(' ');
+      return 'Exec: ' + testCommandOutput.name + ' ' + argsOut;
     }
   },
   {
     item: 'type',
     name: 'optionType',
     parent: 'selection',
     lookup: [
       {
@@ -503,33 +546,33 @@ mockCommands.items = [
             'noerror'
           ]
         }
       }
     ],
     exec: function(args, context) {
       if (args.method === 'reject') {
         return new Promise(function(resolve, reject) {
-          setTimeout(function() {
+          context.environment.window.setTimeout(function() {
             reject('rejected promise');
           }, 10);
         });
       }
 
       if (args.method === 'rejecttyped') {
         return new Promise(function(resolve, reject) {
-          setTimeout(function() {
+          context.environment.window.setTimeout(function() {
             reject(context.typedData('number', 54));
           }, 10);
         });
       }
 
       if (args.method === 'throwinpromise') {
         return new Promise(function(resolve, reject) {
-          setTimeout(function() {
+          context.environment.window.setTimeout(function() {
             resolve('should be lost');
           }, 10);
         }).then(function() {
           var t = null;
           return t.foo;
         });
       }
 
@@ -650,17 +693,17 @@ mockCommands.items = [
     name: 'tsslow',
     params: [
       {
         name: 'hello',
         type: {
           name: 'selection',
           data: function(context) {
             return new Promise(function(resolve, reject) {
-              setTimeout(function() {
+              context.environment.window.setTimeout(function() {
                 resolve([
                   'Shalom', 'Namasté', 'Hallo', 'Dydd-da',
                   'Chào', 'Hej', 'Saluton', 'Sawubona'
                 ]);
               }, 10);
             });
           }
         }
@@ -733,10 +776,21 @@ mockCommands.items = [
           ]
         }
       }
     ],
     returnType: 'json',
     exec: function(args, context) {
       return args;
     }
+  },
+  {
+    item: 'command',
+    name: 'tsres',
+    params: [
+      {
+        name: 'resource',
+        type: 'resource'
+      }
+    ],
+    exec: createExec('tsres'),
   }
 ];
--- a/browser/devtools/debugger/debugger-commands.js
+++ b/browser/devtools/debugger/debugger-commands.js
@@ -1,16 +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/. */
 
 "use strict";
 
 const { Cc, Ci, Cu } = require("chrome");
-const gcli = require("gcli/index");
+const l10n = require("gcli/l10n");
 
 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
 
 /**
  * The commands and converters that are exported to GCLI
  */
 exports.items = [];
 
@@ -62,26 +62,28 @@ function getAllSources(dbg) {
     }));
 }
 
 /**
  * 'break' command
  */
 exports.items.push({
   name: "break",
-  description: gcli.lookup("breakDesc"),
-  manual: gcli.lookup("breakManual")
+  description: l10n.lookup("breakDesc"),
+  manual: l10n.lookup("breakManual")
 });
 
 /**
  * 'break list' command
  */
 exports.items.push({
   name: "break list",
-  description: gcli.lookup("breaklistDesc"),
+  item: "command",
+  runAt: "client",
+  description: l10n.lookup("breaklistDesc"),
   returnType: "breakpoints",
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger", { ensureOpened: true });
     return dbg.then(getAllBreakpoints);
   }
 });
 
 exports.items.push({
@@ -97,17 +99,17 @@ exports.items.push({
           breakpoints: breakpoints,
           onclick: context.update,
           ondblclick: context.updateExec
         }
       });
     } else {
       return context.createView({
         html: "<p>${message}</p>",
-        data: { message: gcli.lookup("breaklistNone") }
+        data: { message: l10n.lookup("breaklistNone") }
       });
     }
   }
 });
 
 var breakListHtml = "" +
       "<table>" +
       " <thead>" +
@@ -121,87 +123,91 @@ var breakListHtml = "" +
       "    <td class='gcli-breakpoint-lineText'>" +
       "      ${breakpoint.truncatedLineText}" +
       "    </td>" +
       "    <td>" +
       "      <span class='gcli-out-shortcut'" +
       "            data-command='break del ${breakpoint.label}'" +
       "            onclick='${onclick}'" +
       "            ondblclick='${ondblclick}'>" +
-      "        " + gcli.lookup("breaklistOutRemove") + "</span>" +
+      "        " + l10n.lookup("breaklistOutRemove") + "</span>" +
       "    </td>" +
       "  </tr>" +
       " </tbody>" +
       "</table>" +
       "";
 
 var MAX_LINE_TEXT_LENGTH = 30;
 var MAX_LABEL_LENGTH = 20;
 
 /**
  * 'break add' command
  */
 exports.items.push({
   name: "break add",
-  description: gcli.lookup("breakaddDesc"),
-  manual: gcli.lookup("breakaddManual")
+  description: l10n.lookup("breakaddDesc"),
+  manual: l10n.lookup("breakaddManual")
 });
 
 /**
  * 'break add line' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: "break add line",
-  description: gcli.lookup("breakaddlineDesc"),
+  description: l10n.lookup("breakaddlineDesc"),
   params: [
     {
       name: "file",
       type: {
         name: "selection",
         lookup: function(context) {
           return getAllSources(getPanel(context, "jsdebugger"));
         }
       },
-      description: gcli.lookup("breakaddlineFileDesc")
+      description: l10n.lookup("breakaddlineFileDesc")
     },
     {
       name: "line",
       type: { name: "number", min: 1, step: 10 },
-      description: gcli.lookup("breakaddlineLineDesc")
+      description: l10n.lookup("breakaddlineLineDesc")
     }
   ],
   returnType: "string",
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
-      return gcli.lookup("debuggerStopped");
+      return l10n.lookup("debuggerStopped");
     }
 
     let deferred = context.defer();
     let item = dbg._view.Sources.getItemForAttachment(a => {
       return a.source && a.source.actor === args.file;
     })
     let position = { actor: item.value, line: args.line };
 
     dbg.addBreakpoint(position).then(() => {
-      deferred.resolve(gcli.lookup("breakaddAdded"));
+      deferred.resolve(l10n.lookup("breakaddAdded"));
     }, aError => {
-      deferred.resolve(gcli.lookupFormat("breakaddFailed", [aError]));
+      deferred.resolve(l10n.lookupFormat("breakaddFailed", [aError]));
     });
 
     return deferred.promise;
   }
 });
 
 /**
  * 'break del' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: "break del",
-  description: gcli.lookup("breakdelDesc"),
+  description: l10n.lookup("breakdelDesc"),
   params: [
     {
       name: "breakpoint",
       type: {
         name: "selection",
         lookup: function(context) {
           let dbg = getPanel(context, "jsdebugger");
           if (!dbg) {
@@ -209,208 +215,226 @@ exports.items.push({
           }
           return getAllBreakpoints(dbg).map(breakpoint => ({
             name: breakpoint.label,
             value: breakpoint,
             description: breakpoint.truncatedLineText
           }));
         }
       },
-      description: gcli.lookup("breakdelBreakidDesc")
+      description: l10n.lookup("breakdelBreakidDesc")
     }
   ],
   returnType: "string",
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
-      return gcli.lookup("debuggerStopped");
+      return l10n.lookup("debuggerStopped");
     }
 
     let source = dbg._view.Sources.getItemForAttachment(a => {
       return a.source && a.source.url === args.breakpoint.url
     });
 
     let deferred = context.defer();
     let position = { actor: source.attachment.source.actor,
                      line: args.breakpoint.lineNumber };
 
     dbg.removeBreakpoint(position).then(() => {
-      deferred.resolve(gcli.lookup("breakdelRemoved"));
+      deferred.resolve(l10n.lookup("breakdelRemoved"));
     }, () => {
-      deferred.resolve(gcli.lookup("breakNotFound"));
+      deferred.resolve(l10n.lookup("breakNotFound"));
     });
 
     return deferred.promise;
   }
 });
 
 /**
  * 'dbg' command
  */
 exports.items.push({
   name: "dbg",
-  description: gcli.lookup("dbgDesc"),
-  manual: gcli.lookup("dbgManual")
+  description: l10n.lookup("dbgDesc"),
+  manual: l10n.lookup("dbgManual")
 });
 
 /**
  * 'dbg open' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: "dbg open",
-  description: gcli.lookup("dbgOpen"),
+  description: l10n.lookup("dbgOpen"),
   params: [],
   exec: function(args, context) {
     let target = context.environment.target;
     return gDevTools.showToolbox(target, "jsdebugger").then(() => null);
   }
 });
 
 /**
  * 'dbg close' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: "dbg close",
-  description: gcli.lookup("dbgClose"),
+  description: l10n.lookup("dbgClose"),
   params: [],
   exec: function(args, context) {
     if (!getPanel(context, "jsdebugger")) {
       return;
     }
     let target = context.environment.target;
     return gDevTools.closeToolbox(target).then(() => null);
   }
 });
 
 /**
  * 'dbg interrupt' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: "dbg interrupt",
-  description: gcli.lookup("dbgInterrupt"),
+  description: l10n.lookup("dbgInterrupt"),
   params: [],
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
-      return gcli.lookup("debuggerStopped");
+      return l10n.lookup("debuggerStopped");
     }
 
     let controller = dbg._controller;
     let thread = controller.activeThread;
     if (!thread.paused) {
       thread.interrupt();
     }
   }
 });
 
 /**
  * 'dbg continue' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: "dbg continue",
-  description: gcli.lookup("dbgContinue"),
+  description: l10n.lookup("dbgContinue"),
   params: [],
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
-      return gcli.lookup("debuggerStopped");
+      return l10n.lookup("debuggerStopped");
     }
 
     let controller = dbg._controller;
     let thread = controller.activeThread;
     if (thread.paused) {
       thread.resume();
     }
   }
 });
 
 /**
  * 'dbg step' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: "dbg step",
-  description: gcli.lookup("dbgStepDesc"),
-  manual: gcli.lookup("dbgStepManual")
+  description: l10n.lookup("dbgStepDesc"),
+  manual: l10n.lookup("dbgStepManual")
 });
 
 /**
  * 'dbg step over' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: "dbg step over",
-  description: gcli.lookup("dbgStepOverDesc"),
+  description: l10n.lookup("dbgStepOverDesc"),
   params: [],
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
-      return gcli.lookup("debuggerStopped");
+      return l10n.lookup("debuggerStopped");
     }
 
     let controller = dbg._controller;
     let thread = controller.activeThread;
     if (thread.paused) {
       thread.stepOver();
     }
   }
 });
 
 /**
  * 'dbg step in' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: 'dbg step in',
-  description: gcli.lookup("dbgStepInDesc"),
+  description: l10n.lookup("dbgStepInDesc"),
   params: [],
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
-      return gcli.lookup("debuggerStopped");
+      return l10n.lookup("debuggerStopped");
     }
 
     let controller = dbg._controller;
     let thread = controller.activeThread;
     if (thread.paused) {
       thread.stepIn();
     }
   }
 });
 
 /**
  * 'dbg step over' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: 'dbg step out',
-  description: gcli.lookup("dbgStepOutDesc"),
+  description: l10n.lookup("dbgStepOutDesc"),
   params: [],
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
-      return gcli.lookup("debuggerStopped");
+      return l10n.lookup("debuggerStopped");
     }
 
     let controller = dbg._controller;
     let thread = controller.activeThread;
     if (thread.paused) {
       thread.stepOut();
     }
   }
 });
 
 /**
  * 'dbg list' command
  */
 exports.items.push({
+  item: "command",
+  runAt: "client",
   name: "dbg list",
-  description: gcli.lookup("dbgListSourcesDesc"),
+  description: l10n.lookup("dbgListSourcesDesc"),
   params: [],
   returnType: "dom",
   exec: function(args, context) {
     let dbg = getPanel(context, "jsdebugger");
     if (!dbg) {
-      return gcli.lookup("debuggerClosed");
+      return l10n.lookup("debuggerClosed");
     }
 
     let sources = getAllSources(dbg);
     let doc = context.environment.chromeDocument;
     let div = createXHTMLElement(doc, "div");
     let ol = createXHTMLElement(doc, "ol");
 
     sources.forEach(source => {
@@ -435,20 +459,22 @@ exports.items.push({
   },
   {
     name: "unblackbox",
     clientMethod: "unblackBox",
     l10nPrefix: "dbgUnBlackBox"
   }
 ].forEach(function(cmd) {
   const lookup = function(id) {
-    return gcli.lookup(cmd.l10nPrefix + id);
+    return l10n.lookup(cmd.l10nPrefix + id);
   };
 
   exports.items.push({
+    item: "command",
+    runAt: "client",
     name: "dbg " + cmd.name,
     description: lookup("Desc"),
     params: [
       {
         name: "source",
         type: {
           name: "selection",
           lookup: function(context) {
@@ -470,17 +496,17 @@ exports.items.push({
         description: lookup("InvertDesc")
       }
     ],
     returnType: "dom",
     exec: function(args, context) {
       const dbg = getPanel(context, "jsdebugger");
       const doc = context.environment.chromeDocument;
       if (!dbg) {
-        throw new Error(gcli.lookup("debuggerClosed"));
+        throw new Error(l10n.lookup("debuggerClosed"));
       }
 
       const { promise, resolve, reject } = context.defer();
       const { activeThread } = dbg._controller;
       const globRegExp = args.glob ? globToRegExp(args.glob) : null;
 
       // Filter the sources down to those that we will need to black box.
 
copy from browser/devtools/main.js
copy to browser/devtools/definitions.js
--- a/browser/devtools/main.js
+++ b/browser/devtools/definitions.js
@@ -1,31 +1,20 @@
 /* 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");
 
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource:///modules/devtools/gDevTools.jsm");
-
-Object.defineProperty(exports, "Toolbox", {
-  get: () => require("devtools/framework/toolbox").Toolbox
-});
-Object.defineProperty(exports, "TargetFactory", {
-  get: () => require("devtools/framework/target").TargetFactory
-});
+const { Services } = require("resource://gre/modules/Services.jsm");
 
 loader.lazyGetter(this, "osString", () => Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS);
 
-let events = require("sdk/system/events");
-
 // Panels
 loader.lazyGetter(this, "OptionsPanel", () => require("devtools/framework/toolbox-options").OptionsPanel);
 loader.lazyGetter(this, "InspectorPanel", () => require("devtools/inspector/inspector-panel").InspectorPanel);
 loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/panel").WebConsolePanel);
 loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/panel").DebuggerPanel);
 loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/styleeditor/styleeditor-panel").StyleEditorPanel);
 loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel);
 loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/canvasdebugger/panel").CanvasDebuggerPanel);
@@ -374,62 +363,37 @@ let defaultTools = [
   Tools.performance,
   Tools.netMonitor,
   Tools.storage,
   Tools.scratchpad
 ];
 
 exports.defaultTools = defaultTools;
 
-for (let definition of defaultTools) {
-  gDevTools.registerTool(definition);
-}
-
 Tools.darkTheme = {
   id: "dark",
   label: l10n("options.darkTheme.label", toolboxStrings),
   ordinal: 1,
   stylesheets: ["chrome://browser/skin/devtools/dark-theme.css"],
   classList: ["theme-dark"],
 };
 
 Tools.lightTheme = {
   id: "light",
   label: l10n("options.lightTheme.label", toolboxStrings),
   ordinal: 2,
   stylesheets: ["chrome://browser/skin/devtools/light-theme.css"],
   classList: ["theme-light"],
 };
 
-let defaultThemes = [
+exports.defaultThemes = [
   Tools.darkTheme,
   Tools.lightTheme,
 ];
 
-for (let definition of defaultThemes) {
-  gDevTools.registerTheme(definition);
-}
-
-var unloadObserver = {
-  observe: function(subject, topic, data) {
-    if (subject.wrappedJSObject === require("@loader/unload")) {
-      Services.obs.removeObserver(unloadObserver, "sdk:loader:destroy");
-      for (let definition of gDevTools.getToolDefinitionArray()) {
-        gDevTools.unregisterTool(definition.id);
-      }
-      for (let definition of gDevTools.getThemeDefinitionArray()) {
-        gDevTools.unregisterTheme(definition.id);
-      }
-    }
-  }
-};
-Services.obs.addObserver(unloadObserver, "sdk:loader:destroy", false);
-
-events.emit("devtools-loaded", {});
-
 /**
  * Lookup l10n string from a string bundle.
  *
  * @param {string} name
  *        The key to lookup.
  * @param {StringBundle} bundle
  *        The key to lookup.
  * @returns A localized version of the given key.
--- a/browser/devtools/eyedropper/commands.js
+++ b/browser/devtools/eyedropper/commands.js
@@ -1,30 +1,35 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const gcli = require("gcli/index");
+const l10n = require("gcli/l10n");
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const eventEmitter = new EventEmitter();
 
 let { Eyedropper, EyedropperManager } = require("devtools/eyedropper/eyedropper");
 
 /**
  * 'eyedropper' command
  */
 exports.items = [{
+  item: "command",
+  runAt: "client",
   name: "eyedropper",
-  description: gcli.lookup("eyedropperDesc"),
-  manual: gcli.lookup("eyedropperManual"),
+  description: l10n.lookup("eyedropperDesc"),
+  manual: l10n.lookup("eyedropperManual"),
   buttonId: "command-button-eyedropper",
   buttonClass: "command-button command-button-invertable",
-  tooltipText: gcli.lookup("eyedropperTooltip"),
+  tooltipText: l10n.lookup("eyedropperTooltip"),
   state: {
     isChecked: function(target) {
+      if (!target.tab) {
+        return false;
+      }
       let chromeWindow = target.tab.ownerDocument.defaultView;
       let dropper = EyedropperManager.getInstance(chromeWindow);
       if (dropper) {
         return true;
       }
       return false;
     },
     onChange: function(target, changeHandler) {
--- a/browser/devtools/framework/sidebar.js
+++ b/browser/devtools/framework/sidebar.js
@@ -115,28 +115,30 @@ ToolSidebar.prototype = {
   addAllTabsMenu: function() {
     if (this._allTabsBtn) {
       return;
     }
 
     let tabs = this._tabbox.tabs;
 
     // Create a container and insert it first in the tabbox
-    let allTabsContainer = this._panelDoc.createElementNS(XULNS, "box");
+    let allTabsContainer = this._panelDoc.createElementNS(XULNS, "stack");
     this._tabbox.insertBefore(allTabsContainer, tabs);
 
     // Move the tabs inside and make them flex
     allTabsContainer.appendChild(tabs);
     tabs.setAttribute("flex", "1");
 
     // Create the dropdown menu next to the tabs
     this._allTabsBtn = this._panelDoc.createElementNS(XULNS, "toolbarbutton");
     this._allTabsBtn.setAttribute("class", "devtools-sidebar-alltabs");
+    this._allTabsBtn.setAttribute("right", "0");
+    this._allTabsBtn.setAttribute("top", "0");
+    this._allTabsBtn.setAttribute("width", "15");
     this._allTabsBtn.setAttribute("type", "menu");
-    this._allTabsBtn.setAttribute("label", l10n("sidebar.showAllTabs.label"));
     this._allTabsBtn.setAttribute("tooltiptext", l10n("sidebar.showAllTabs.tooltip"));
     this._allTabsBtn.setAttribute("hidden", "true");
     allTabsContainer.appendChild(this._allTabsBtn);
 
     let menuPopup = this._panelDoc.createElementNS(XULNS, "menupopup");
     this._allTabsBtn.appendChild(menuPopup);
 
     // Listening to tabs overflow event to toggle the alltabs button
@@ -157,17 +159,17 @@ ToolSidebar.prototype = {
 
     let tabs = this._tabbox.tabs;
 
     tabs.removeEventListener("overflow", this._onTabBoxOverflow, false);
     tabs.removeEventListener("underflow", this._onTabBoxUnderflow, false);
 
     // Moving back the tabs as a first child of the tabbox
     this._tabbox.insertBefore(tabs, this._tabbox.tabpanels);
-    this._tabbox.querySelector("box").remove();
+    this._tabbox.querySelector("stack").remove();
 
     this._allTabsBtn = null;
   },
 
   _onTabBoxOverflow: function() {
     this._allTabsBtn.removeAttribute("hidden");
   },
 
--- a/browser/devtools/framework/test/browser_toolbox_tool_remote_reopen.js
+++ b/browser/devtools/framework/test/browser_toolbox_tool_remote_reopen.js
@@ -113,16 +113,20 @@ function test() {
       for (let actor of pool.__poolMap.keys()) {
         // Bug 1056342: Profiler fails today because of framerate actor, but
         // this appears more complex to rework, so leave it for that bug to
         // resolve.
         if (actor.contains("framerateActor")) {
           todo(false, "Front for " + actor + " still held in pool!");
           continue;
         }
+        // gcliActor is for the commandline which is separate to the toolbox
+        if (actor.contains("gcliActor")) {
+          continue;
+        }
         ok(false, "Front for " + actor + " still held in pool!");
       }
     }
 
     gBrowser.removeCurrentTab();
     DebuggerServer.destroy();
     toggleAllTools(false);
     finish();
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -708,20 +708,23 @@ Toolbox.prototype = {
   /**
    * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
    */
   _buildButtons: function() {
     if (!this.target.isAddon) {
       this._buildPickerButton();
     }
 
-    let spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
-    let environment = CommandUtils.createEnvironment(this, '_target');
-    return CommandUtils.createRequisition(environment).then(requisition => {
+    const options = {
+      environment: CommandUtils.createEnvironment(this, '_target')
+    };
+    return CommandUtils.createRequisition(this.target, options).then(requisition => {
       this._requisition = requisition;
+
+      const spec = CommandUtils.getCommandbarSpec("devtools.toolbox.toolbarSpec");
       return CommandUtils.createButtons(spec, this.target, this.doc,
                                         requisition).then(buttons => {
         let container = this.doc.getElementById("toolbox-buttons");
         buttons.forEach(button=> {
           if (button) {
             container.appendChild(button);
           }
         });
@@ -1747,17 +1750,17 @@ Toolbox.prototype = {
 
     // Destroy the profiler connection
     outstanding.push(this._disconnectProfiler());
 
     // We need to grab a reference to win before this._host is destroyed.
     let win = this.frame.ownerGlobal;
 
     if (this._requisition) {
-      this._requisition.destroy();
+      CommandUtils.destroyRequisition(this._requisition, this.target);
     }
     this._telemetry.toolClosed("toolbox");
     this._telemetry.destroy();
 
     // Finish all outstanding tasks (which means finish destroying panels and
     // then destroying the host, successfully or not) before destroying the
     // target.
     this._destroyer = DevToolsUtils.settleAll(outstanding)
--- a/browser/devtools/inspector/inspector-commands.js
+++ b/browser/devtools/inspector/inspector-commands.js
@@ -1,26 +1,28 @@
 /* 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 gcli = require("gcli/index");
+const l10n = require("gcli/l10n");
 
 exports.items = [{
+  item: "command",
+  runAt: "server",
   name: "inspect",
-  description: gcli.lookup("inspectDesc"),
-  manual: gcli.lookup("inspectManual"),
+  description: l10n.lookup("inspectDesc"),
+  manual: l10n.lookup("inspectManual"),
   params: [
     {
       name: "selector",
       type: "node",
-      description: gcli.lookup("inspectNodeDesc"),
-      manual: gcli.lookup("inspectNodeManual")
+      description: l10n.lookup("inspectNodeDesc"),
+      manual: l10n.lookup("inspectNodeManual")
     }
   ],
   exec: function(args, context) {
     let target = context.environment.target;
     let gDevTools = require("resource:///modules/devtools/gDevTools.jsm").gDevTools;
 
     return gDevTools.showToolbox(target, "inspector").then(toolbox => {
       toolbox.getCurrentPanel().selection.setNode(args.selector, "gcli");
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -828,16 +828,22 @@ InspectorPanel.prototype = {
    * When the pane toggle button is clicked, toggle the pane, change the button
    * state and tooltip.
    */
   onPaneToggleButtonClicked: function(e) {
     let sidePane = this.panelDoc.querySelector("#inspector-sidebar");
     let button = this._paneToggleButton;
     let isVisible = !button.hasAttribute("pane-collapsed");
 
+    // Make sure the sidebar has a width attribute before collapsing because
+    // ViewHelpers needs it.
+    if (isVisible && !sidePane.hasAttribute("width")) {
+      sidePane.setAttribute("width", sidePane.getBoundingClientRect().width);
+    }
+
     ViewHelpers.togglePane({
       visible: !isVisible,
       animated: true,
       delayed: true
     }, sidePane);
 
     if (isVisible) {
       button.setAttribute("pane-collapsed", "");
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -1,445 +1,45 @@
 /* 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 { Cu } = require("chrome");
+Cu.import("resource://gre/modules/Services.jsm");
+const { gDevTools } = require("resource:///modules/devtools/gDevTools.jsm");
+
+const { defaultTools, defaultThemes } = require("definitions");
 
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource:///modules/devtools/gDevTools.jsm");
+defaultTools.forEach(definition => gDevTools.registerTool(definition));
+defaultThemes.forEach(definition => gDevTools.registerTheme(definition));
+
+// Re-export for backwards compatibility, but we should probably the
+// definitions from require("definitions") in the future
+exports.defaultTools = require("definitions").defaultTools;
+exports.defaultThemes = require("definitions").defaultThemes;
+exports.Tools = require("definitions").Tools;
 
 Object.defineProperty(exports, "Toolbox", {
   get: () => require("devtools/framework/toolbox").Toolbox
 });
 Object.defineProperty(exports, "TargetFactory", {
   get: () => require("devtools/framework/target").TargetFactory
 });
 
-loader.lazyGetter(this, "osString", () => Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS);
-
-let events = require("sdk/system/events");
-
-// Panels
-loader.lazyGetter(this, "OptionsPanel", () => require("devtools/framework/toolbox-options").OptionsPanel);
-loader.lazyGetter(this, "InspectorPanel", () => require("devtools/inspector/inspector-panel").InspectorPanel);
-loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/panel").WebConsolePanel);
-loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/panel").DebuggerPanel);
-loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/styleeditor/styleeditor-panel").StyleEditorPanel);
-loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel);
-loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/canvasdebugger/panel").CanvasDebuggerPanel);
-loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/webaudioeditor/panel").WebAudioEditorPanel);
-loader.lazyGetter(this, "PerformancePanel", () => require("devtools/performance/panel").PerformancePanel);
-loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/panel").NetMonitorPanel);
-loader.lazyGetter(this, "StoragePanel", () => require("devtools/storage/panel").StoragePanel);
-loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel);
-
-// Strings
-const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties";
-const inspectorProps = "chrome://browser/locale/devtools/inspector.properties";
-const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties";
-const debuggerProps = "chrome://browser/locale/devtools/debugger.properties";
-const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties";
-const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties";
-const canvasDebuggerProps = "chrome://browser/locale/devtools/canvasdebugger.properties";
-const webAudioEditorProps = "chrome://browser/locale/devtools/webaudioeditor.properties";
-const profilerProps = "chrome://browser/locale/devtools/profiler.properties";
-const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties";
-const storageProps = "chrome://browser/locale/devtools/storage.properties";
-const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties";
-
-loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(toolboxProps));
-loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps));
-loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps));
-loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps));
-loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps));
-loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBundle(shaderEditorProps));
-loader.lazyGetter(this, "canvasDebuggerStrings", () => Services.strings.createBundle(canvasDebuggerProps));
-loader.lazyGetter(this, "webAudioEditorStrings", () => Services.strings.createBundle(webAudioEditorProps));
-loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps));
-loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps));
-loader.lazyGetter(this, "storageStrings", () => Services.strings.createBundle(storageProps));
-loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps));
-
-let Tools = {};
-exports.Tools = Tools;
-
-// Definitions
-Tools.options = {
-  id: "options",
-  ordinal: 0,
-  url: "chrome://browser/content/devtools/framework/toolbox-options.xul",
-  icon: "chrome://browser/skin/devtools/tool-options.svg",
-  invertIconForLightTheme: true,
-  bgTheme: "theme-body",
-  label: l10n("options.label", toolboxStrings),
-  iconOnly: true,
-  panelLabel: l10n("options.panelLabel", toolboxStrings),
-  tooltip: l10n("optionsButton.tooltip", toolboxStrings),
-  inMenu: false,
-
-  isTargetSupported: function(target) {
-    return true;
-  },
-
-  build: function(iframeWindow, toolbox) {
-    return new OptionsPanel(iframeWindow, toolbox);
-  }
-}
-
-Tools.inspector = {
-  id: "inspector",
-  accesskey: l10n("inspector.accesskey", inspectorStrings),
-  key: l10n("inspector.commandkey", inspectorStrings),
-  ordinal: 1,
-  modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
-  icon: "chrome://browser/skin/devtools/tool-inspector.svg",
-  invertIconForLightTheme: true,
-  url: "chrome://browser/content/devtools/inspector/inspector.xul",
-  label: l10n("inspector.label", inspectorStrings),
-  panelLabel: l10n("inspector.panelLabel", inspectorStrings),
-  tooltip: l10n("inspector.tooltip", inspectorStrings),
-  inMenu: true,
-  commands: [
-    "devtools/resize-commands",
-    "devtools/inspector/inspector-commands",
-    "devtools/eyedropper/commands.js"
-  ],
-
-  preventClosingOnKey: true,
-  onkey: function(panel) {
-    panel.toolbox.highlighterUtils.togglePicker();
-  },
-
-  isTargetSupported: function(target) {
-    return target.hasActor("inspector");
-  },
-
-  build: function(iframeWindow, toolbox) {
-    return new InspectorPanel(iframeWindow, toolbox);
-  }
-};
-
-Tools.webConsole = {
-  id: "webconsole",
-  key: l10n("cmd.commandkey", webConsoleStrings),
-  accesskey: l10n("webConsoleCmd.accesskey", webConsoleStrings),
-  modifiers: Services.appinfo.OS == "Darwin" ? "accel,alt" : "accel,shift",
-  ordinal: 2,
-  icon: "chrome://browser/skin/devtools/tool-webconsole.svg",
-  invertIconForLightTheme: true,
-  url: "chrome://browser/content/devtools/webconsole.xul",
-  label: l10n("ToolboxTabWebconsole.label", webConsoleStrings),
-  menuLabel: l10n("MenuWebconsole.label", webConsoleStrings),
-  panelLabel: l10n("ToolboxWebConsole.panelLabel", webConsoleStrings),
-  tool