Merge m-c to s-c.
authorRichard Newman <rnewman@mozilla.com>
Fri, 09 Dec 2011 00:14:26 -0800
changeset 84628 5707dd62841423a9bf2428f15f96b870b2d52f95
parent 84627 bd58575a3bbefface54f8d09d37bdc9b47d60e58 (current diff)
parent 83908 9e7239c0f557ddafdca5bcf9598294c52f42d08d (diff)
child 84629 4bc459c8af2438056540fa2794147ea473666155
push id519
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 00:38:35 +0000
treeherdermozilla-beta@788ea1ef610b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone11.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 s-c.
--- a/browser/components/tabview/test/Makefile.in
+++ b/browser/components/tabview/test/Makefile.in
@@ -164,16 +164,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug685476.js \
                  browser_tabview_bug685692.js \
                  browser_tabview_bug686654.js \
                  browser_tabview_bug696602.js \
                  browser_tabview_bug697390.js \
                  browser_tabview_bug705621.js \
                  browser_tabview_bug706430.js \
                  browser_tabview_bug706736.js \
+                 browser_tabview_bug707466.js \
                  browser_tabview_click_group.js \
                  browser_tabview_dragdrop.js \
                  browser_tabview_exit_button.js \
                  browser_tabview_expander.js \
                  browser_tabview_firstrun_pref.js \
                  browser_tabview_group.js \
                  browser_tabview_launch.js \
                  browser_tabview_multiwindow_search.js \
new file mode 100644
--- /dev/null
+++ b/browser/components/tabview/test/browser_tabview_bug707466.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+  waitForExplicitFinish();
+
+  // create two groups and each group has one tab item
+  let newState = {
+    windows: [{
+      tabs: [{
+        entries: [{ url: "about:blank" }],
+        hidden: true,
+        attributes: {},
+        extData: {
+          "tabview-tab":
+            '{"bounds":{"left":21,"top":29,"width":204,"height":153},' +
+            '"userSize":null,"url":"about:blank","groupID":1,' +
+            '"imageData":null,"title":null}'
+        }
+      },{
+        entries: [{ url: "about:blank" }],
+        hidden: false,
+        attributes: {},
+        extData: {
+          "tabview-tab":
+            '{"bounds":{"left":315,"top":29,"width":111,"height":84},' +
+            '"userSize":null,"url":"about:blank","groupID":2,' +
+            '"imageData":null,"title":null}'
+        },
+      }],
+      selected:2,
+      _closedTabs: [],
+      extData: {
+        "tabview-groups": '{"nextID":3,"activeGroupId":2}',
+        "tabview-group":
+          '{"1":{"bounds":{"left":15,"top":5,"width":280,"height":232},' +
+          '"userSize":null,"title":"","id":1},' +
+          '"2":{"bounds":{"left":309,"top":5,"width":267,"height":226},' +
+          '"userSize":null,"title":"","id":2}}',
+        "tabview-ui": '{"pageBounds":{"left":0,"top":0,"width":788,"height":548}}'
+      }, sizemode:"normal"
+    }]
+  };
+
+  newWindowWithState(newState, function(win) {
+    registerCleanupFunction(function () win.close());
+
+    whenTabViewIsShown(function() {
+      let cw = win.TabView.getContentWindow();
+
+      is(cw.GroupItems.groupItems.length, 2, "There are still two groups");
+      is(win.gBrowser.tabs.length, 1, "There is only one tab");
+      is(cw.UI.getActiveTab(), win.gBrowser.selectedTab._tabViewTabItem, "The last tab is selected");
+
+      finish();
+    }, win);
+    win.gBrowser.removeTab(win.gBrowser.selectedTab);
+  });
+}
+
--- a/browser/components/tabview/ui.js
+++ b/browser/components/tabview/ui.js
@@ -470,17 +470,17 @@ let UI = {
     if (item.isATabItem) {
       if (item.parent)
         GroupItems.setActiveGroupItem(item.parent);
       if (!options || !options.dontSetActiveTabInGroup)
         this._setActiveTab(item);
     } else {
       GroupItems.setActiveGroupItem(item);
       if (!options || !options.dontSetActiveTabInGroup) {
-        let activeTab = item.getActiveTab()
+        let activeTab = item.getActiveTab();
         if (activeTab)
           this._setActiveTab(activeTab);
       }
     }
   },
 
   // ----------
   // Function: clearActiveTab
@@ -569,17 +569,18 @@ let UI = {
         dispatchEvent(event);
 
         // Flush pending updates
         GroupItems.flushAppTabUpdates();
 
         TabItems.resumePainting();
       });
     } else {
-      self.clearActiveTab();
+      if (!currentTab || !currentTab._tabViewTabItem)
+        self.clearActiveTab();
       self._isChangingVisibility = false;
       dispatchEvent(event);
 
       // Flush pending updates
       GroupItems.flushAppTabUpdates();
 
       TabItems.resumePainting();
     }
--- a/browser/devtools/shared/Templater.jsm
+++ b/browser/devtools/shared/Templater.jsm
@@ -32,31 +32,72 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 
-var EXPORTED_SYMBOLS = [ "Templater" ];
+var EXPORTED_SYMBOLS = [ "Templater", "template" ];
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 const Node = Components.interfaces.nsIDOMNode;
 
 // WARNING: do not 'use_strict' without reading the notes in _envEval();
 
 /**
- * A templater that allows one to quickly template DOM nodes.
+ * Begin a new templating process.
+ * @param node A DOM element or string referring to an element's id
+ * @param data Data to use in filling out the template
+ * @param options Options to customize the template processing. One of:
+ * - allowEval: boolean (default false) Basic template interpolations are
+ * either property paths (e.g. ${a.b.c.d}), however if allowEval=true then we
+ * allow arbitrary JavaScript
  */
-function Templater() {
+function template(node, data, options) {
+  var template = new Templater(options || {});
+  template.processNode(node, data);
+  return template;
+}
+
+/**
+ * Construct a Templater object. Use template() in preference to this ctor.
+ * @deprecated Use template(node, data, options);
+ */
+function Templater(options) {
+  if (options == null) {
+    options = { allowEval: true };
+  }
+  this.options = options;
   this.stack = [];
 }
 
 /**
+ * Cached regex used to find ${...} sections in some text.
+ * Performance note: This regex uses ( and ) to capture the 'script' for
+ * further processing. Not all of the uses of this regex use this feature so
+ * if use of the capturing group is a performance drain then we should split
+ * this regex in two.
+ */
+Templater.prototype._templateRegion = /\$\{([^}]*)\}/g;
+
+/**
+ * Cached regex used to split a string using the unicode chars F001 and F002.
+ * See Templater._processTextNode() for details.
+ */
+Templater.prototype._splitSpecial = /\uF001|\uF002/;
+
+/**
+ * Cached regex used to detect if a script is capable of being interpreted
+ * using Template._property() or if we need to use Template._envEval()
+ */
+Templater.prototype._isPropertyScript = /^[a-zA-Z0-9.]*$/;
+
+/**
  * Recursive function to walk the tree processing the attributes as it goes.
  * @param node the node to process. If you pass a string in instead of a DOM
  * element, it is assumed to be an id for use with document.getElementById()
  * @param data the data to use for node processing.
  */
 Templater.prototype.processNode = function(node, data) {
   if (typeof node === 'string') {
     node = document.getElementById(node);
@@ -106,17 +147,17 @@ Templater.prototype.processNode = functi
             node.removeAttribute(name);
             var capture = node.hasAttribute('capture' + name.substring(2));
             node.addEventListener(name.substring(2), func, capture);
             if (capture) {
               node.removeAttribute('capture' + name.substring(2));
             }
           } else {
             // Replace references in all other attributes
-            var newValue = value.replace(/\$\{[^}]*\}/g, function(path) {
+            var newValue = value.replace(this._templateRegion, function(path) {
               return this._envEval(path.slice(2, -1), data, value);
             }.bind(this));
             // Remove '_' prefix of attribute names so the DOM won't try
             // to use them before we've processed the template
             if (name.charAt(0) === '_') {
               node.removeAttribute(name);
               node.setAttribute(name.substring(1), newValue);
             } else if (value !== newValue) {
@@ -290,18 +331,18 @@ Templater.prototype._processTextNode = f
   // attribute processing in processNode()) because we need to support
   // functions that return DOM nodes, so we can't have the conversion to a
   // string.
   // Instead we process the string as an array of parts. In order to split
   // the string up, we first replace '${' with '\uF001$' and '}' with '\uF002'
   // We can then split using \uF001 or \uF002 to get an array of strings
   // where scripts are prefixed with $.
   // \uF001 and \uF002 are just unicode chars reserved for private use.
-  value = value.replace(/\$\{([^}]*)\}/g, '\uF001$$$1\uF002');
-  var parts = value.split(/\uF001|\uF002/);
+  value = value.replace(this._templateRegion, '\uF001$$$1\uF002');
+  var parts = value.split(this._splitSpecial);
   if (parts.length > 1) {
     parts.forEach(function(part) {
       if (part === null || part === undefined || part === '') {
         return;
       }
       if (part.charAt(0) === '$') {
         part = this._envEval(part.slice(1), data, node.data);
       }
@@ -358,17 +399,17 @@ Templater.prototype._handleAsync = funct
 };
 
 /**
  * Warn of string does not begin '${' and end '}'
  * @param str the string to check.
  * @return The string stripped of ${ and }, or untouched if it does not match
  */
 Templater.prototype._stripBraces = function(str) {
-  if (!str.match(/\$\{.*\}/g)) {
+  if (!str.match(this._templateRegion)) {
     this._handleError('Expected ' + str + ' to match ${...}');
     return str;
   }
   return str.slice(2, -1);
 };
 
 /**
  * Combined getter and setter that works with a path through some data set.
@@ -422,27 +463,36 @@ Templater.prototype._property = function
  * the values in the env object. This is likely to be slow, but workable.
  * @param script The string to be evaluated.
  * @param data The environment in which to eval the script.
  * @param frame Optional debugging string in case of failure.
  * @return The return value of the script, or the error message if the script
  * execution failed.
  */
 Templater.prototype._envEval = function(script, data, frame) {
-  with (data) {
-    try {
-      this.stack.push(frame);
-      return eval(script);
-    } catch (ex) {
-      this._handleError('Template error evaluating \'' + script + '\'' +
-          ' environment=' + Object.keys(data).join(', '), ex);
-      return script;
-    } finally {
-      this.stack.pop();
+  try {
+    this.stack.push(frame);
+    if (this._isPropertyScript.test(script)) {
+      return this._property(script, data);
+    } else {
+      if (!this.options.allowEval) {
+        this._handleError('allowEval is not set, however \'' + script + '\'' +
+            ' can not be resolved using a simple property path.');
+        return '${' + script + '}';
+      }
+      with (data) {
+        return eval(script);
+      }
     }
+  } catch (ex) {
+    this._handleError('Template error evaluating \'' + script + '\'' +
+        ' environment=' + Object.keys(data).join(', '), ex);
+    return '${' + script + '}';
+  } finally {
+    this.stack.pop();
   }
 };
 
 /**
  * A generic way of reporting errors, for easy overloading in different
  * environments.
  * @param message the error message to report.
  * @param ex optional associated exception.
--- a/browser/devtools/shared/test/browser_templater_basic.js
+++ b/browser/devtools/shared/test/browser_templater_basic.js
@@ -17,17 +17,17 @@ function runTest(index) {
   var options = tests[index] = tests[index]();
   var holder = content.document.createElement('div');
   holder.id = options.name;
   var body = content.document.body;
   body.appendChild(holder);
   holder.innerHTML = options.template;
 
   info('Running ' + options.name);
-  new Templater().processNode(holder, options.data);
+  template(holder, options.data, options.options);
 
   if (typeof options.result == 'string') {
     is(holder.innerHTML, options.result, options.name);
   }
   else {
     ok(holder.innerHTML.match(options.result), options.name);
   }
 
@@ -83,55 +83,60 @@ var tests = [
     template: '<div id="ex1">${nested.value}</div>',
     data: { nested:{ value:'pass 1' } },
     result: '<div id="ex1">pass 1</div>'
   };},
 
   function() { return {
     name: 'returnDom',
     template: '<div id="ex2">${__element.ownerDocument.createTextNode(\'pass 2\')}</div>',
+    options: { allowEval: true },
     data: {},
     result: '<div id="ex2">pass 2</div>'
   };},
 
   function() { return {
     name: 'srcChange',
     template: '<img _src="${fred}" id="ex3">',
     data: { fred:'green.png' },
     result: /<img( id="ex3")? src="green.png"( id="ex3")?>/
   };},
 
   function() { return {
     name: 'ifTrue',
     template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+    options: { allowEval: true },
     data: { name: 'fred' },
     result: '<p>hello fred</p>'
   };},
 
   function() { return {
     name: 'ifFalse',
     template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+    options: { allowEval: true },
     data: { name: 'jim' },
     result: ''
   };},
 
   function() { return {
     name: 'simpleLoop',
     template: '<p foreach="index in ${[ 1, 2, 3 ]}">${index}</p>',
+    options: { allowEval: true },
     data: {},
     result: '<p>1</p><p>2</p><p>3</p>'
   };},
 
   function() { return {
     name: 'loopElement',
     template: '<loop foreach="i in ${array}">${i}</loop>',
     data: { array: [ 1, 2, 3 ] },
     result: '123'
   };},
 
+  // Bug 692028: DOMTemplate memory leak with asynchronous arrays
   // Bug 692031: DOMTemplate async loops do not drop the loop element
   function() { return {
     name: 'asyncLoopElement',
     template: '<loop foreach="i in ${array}">${i}</loop>',
     data: { array: delayReply([1, 2, 3]) },
     result: '<span></span>',
     later: '123'
   };},
@@ -145,16 +150,17 @@ var tests = [
       ok(options.data.element.innerHTML, 'pass 9', 'saveElement saved');
       delete options.data.element;
     }
   };},
 
   function() { return {
     name: 'useElement',
     template: '<p id="pass9">${adjust(__element)}</p>',
+    options: { allowEval: true },
     data: {
       adjust: function(element) {
         is('pass9', element.id, 'useElement adjust');
         return 'pass 9b'
       }
     },
     result: '<p id="pass9">pass 9b</p>'
   };},
@@ -162,44 +168,78 @@ var tests = [
   function() { return {
     name: 'asyncInline',
     template: '${delayed}',
     data: { delayed: delayReply('inline') },
     result: '<span></span>',
     later: 'inline'
   };},
 
+  // Bug 692028: DOMTemplate memory leak with asynchronous arrays
   function() { return {
     name: 'asyncArray',
     template: '<p foreach="i in ${delayed}">${i}</p>',
     data: { delayed: delayReply([1, 2, 3]) },
     result: '<span></span>',
     later: '<p>1</p><p>2</p><p>3</p>'
   };},
 
   function() { return {
     name: 'asyncMember',
     template: '<p foreach="i in ${delayed}">${i}</p>',
     data: { delayed: [delayReply(4), delayReply(5), delayReply(6)] },
     result: '<span></span><span></span><span></span>',
     later: '<p>4</p><p>5</p><p>6</p>'
   };},
 
+  // Bug 692028: DOMTemplate memory leak with asynchronous arrays
   function() { return {
     name: 'asyncBoth',
     template: '<p foreach="i in ${delayed}">${i}</p>',
     data: {
       delayed: delayReply([
         delayReply(4),
         delayReply(5),
         delayReply(6)
       ])
     },
     result: '<span></span>',
     later: '<p>4</p><p>5</p><p>6</p>'
+  };},
+
+  // Bug 701762: DOMTemplate fails when ${foo()} returns undefined
+  function() { return {
+    name: 'functionReturningUndefiend',
+    template: '<p>${foo()}</p>',
+    options: { allowEval: true },
+    data: {
+      foo: function() {}
+    },
+    result: '<p>undefined</p>'
+  };},
+
+  // Bug 702642: DOMTemplate is relatively slow when evaluating JS ${}
+  function() { return {
+    name: 'propertySimple',
+    template: '<p>${a.b.c}</p>',
+    data: { a: { b: { c: 'hello' } } },
+    result: '<p>hello</p>'
+  };},
+
+  function() { return {
+    name: 'propertyPass',
+    template: '<p>${Math.max(1, 2)}</p>',
+    options: { allowEval: true },
+    result: '<p>2</p>'
+  };},
+
+  function() { return {
+    name: 'propertyFail',
+    template: '<p>${Math.max(1, 2)}</p>',
+    result: '<p>${Math.max(1, 2)}</p>'
   };}
 ];
 
 function delayReply(data) {
   var p = new Promise();
   executeSoon(function() {
     p.resolve(data);
   });
--- a/browser/devtools/styleinspector/CssHtmlTree.jsm
+++ b/browser/devtools/styleinspector/CssHtmlTree.jsm
@@ -209,17 +209,20 @@ CssHtmlTree.processTemplate = function C
 {
   if (!aPreserveDestination) {
     aDestination.innerHTML = "";
   }
 
   // All the templater does is to populate a given DOM tree with the given
   // values, so we need to clone the template first.
   let duplicated = aTemplate.cloneNode(true);
-  new Templater().processNode(duplicated, aData);
+
+  // See https://github.com/mozilla/domtemplate/blob/master/README.md
+  // for docs on the template() function
+  template(duplicated, aData, { allowEval: true });
   while (duplicated.firstChild) {
     aDestination.appendChild(duplicated.firstChild);
   }
 };
 
 XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings
         .createBundle("chrome://browser/locale/devtools/styleinspector.properties"));
 
--- a/browser/devtools/webconsole/GcliCommands.jsm
+++ b/browser/devtools/webconsole/GcliCommands.jsm
@@ -57,50 +57,16 @@ gcli.addCommand({
   ],
   returnType: "string",
   exec: function Command_echo(args, context) {
     return args.message;
   }
 });
 
 
-let canon = gcli._internal.require("gcli/canon");
-
-/**
- * 'help' command
- */
-gcli.addCommand({
-  name: "help",
-  returnType: "html",
-  description: gcli.lookup("helpDesc"),
-  exec: function Command_help(args, context) {
-    let output = [];
-
-    output.push("<strong>" + gcli.lookup("helpAvailable") + ":</strong><br/>");
-
-    let commandNames = canon.getCommandNames();
-    commandNames.sort();
-
-    output.push("<table>");
-    for (let i = 0; i < commandNames.length; i++) {
-      let command = canon.getCommand(commandNames[i]);
-      if (!command.hidden && command.description) {
-        output.push("<tr>");
-        output.push('<th class="gcli-help-right">' + command.name + "</th>");
-        output.push("<td>&#x2192; " + command.description + "</td>");
-        output.push("</tr>");
-      }
-    }
-    output.push("</table>");
-
-    return output.join("");
-  }
-});
-
-
 /**
  * 'console' command
  */
 gcli.addCommand({
   name: "console",
   description: gcli.lookup("consoleDesc"),
   manual: gcli.lookup("consoleManual")
 });
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -87,16 +87,22 @@ XPCOMUtils.defineLazyGetter(this, "CssRu
 });
 
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function () {
   var obj = {};
   Cu.import("resource://gre/modules/NetUtil.jsm", obj);
   return obj.NetUtil;
 });
 
+XPCOMUtils.defineLazyGetter(this, "template", function () {
+  var obj = {};
+  Cu.import("resource:///modules/devtools/Templater.jsm", obj);
+  return obj.template;
+});
+
 XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () {
   var obj = {};
   try {
     Cu.import("resource:///modules/PropertyPanel.jsm", obj);
   } catch (err) {
     Cu.reportError(err);
   }
   return obj.PropertyPanel;
@@ -6849,24 +6855,48 @@ GcliTerm.prototype = {
     this.writeOutput(aEvent.output.typed, { category: CATEGORY_INPUT });
 
     if (aEvent.output.output == null) {
       return;
     }
 
     let output = aEvent.output.output;
     if (aEvent.output.command.returnType == "html" && typeof output == "string") {
-      let frag = this.document.createRange().createContextualFragment(
+      output = this.document.createRange().createContextualFragment(
           '<div xmlns="' + HTML_NS + '" xmlns:xul="' + XUL_NS + '">' +
-          output + '</div>');
-
-      output = this.document.createElementNS(HTML_NS, "div");
-      output.appendChild(frag);
-    }
-    this.writeOutput(output);
+          output + '</div>').firstChild;
+    }
+
+    // See https://github.com/mozilla/domtemplate/blob/master/README.md
+    // for docs on the template() function
+    let element = this.document.createRange().createContextualFragment(
+      '<richlistitem xmlns="' + XUL_NS + '" clipboardText="${clipboardText}"' +
+      '    timestamp="${timestamp}" id="${id}" class="hud-msg-node">' +
+      '  <label class="webconsole-timestamp" value="${timestampString}"/>' +
+      '  <vbox class="webconsole-msg-icon-container" style="${iconContainerStyle}">' +
+      '    <image class="webconsole-msg-icon"/>' +
+      '    <spacer flex="1"/>' +
+      '  </vbox>' +
+      '  <hbox flex="1" class="gcliterm-msg-body">${output}</hbox>' +
+      '  <hbox align="start"><label value="1" class="webconsole-msg-repeat"/></hbox>' +
+      '</richlistitem>').firstChild;
+
+    let hud = HUDService.getHudReferenceById(this.hudId);
+    let timestamp = ConsoleUtils.timestamp();
+    template(element, {
+      iconContainerStyle: "margin-left=" + (hud.groupDepth * GROUP_INDENT) + "px",
+      output: output,
+      timestamp: timestamp,
+      timestampString: ConsoleUtils.timestampString(timestamp),
+      clipboardText: output.innerText,
+      id: "console-msg-" + HUDService.sequenceId()
+    });
+
+    ConsoleUtils.setMessageType(element, CATEGORY_OUTPUT, SEVERITY_LOG);
+    ConsoleUtils.outputMessageNode(element, this.hudId);
   },
 
   /**
    * Setup the eval sandbox, should be called whenever we are attached.
    */
   createSandbox: function Gcli_createSandbox()
   {
     let win = this.context.get().QueryInterface(Ci.nsIDOMWindow);
--- a/browser/devtools/webconsole/gcli.jsm
+++ b/browser/devtools/webconsole/gcli.jsm
@@ -681,32 +681,33 @@ var mozl10n = {};
     }
     catch (ex) {
       throw new Error("Failure in lookupFormat('" + name + "')");
     }
   };
 
 })(mozl10n);
 
-define('gcli/index', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types/basic', 'gcli/types/javascript', 'gcli/types/node', 'gcli/cli', 'gcli/ui/display'], function(require, exports, module) {
+define('gcli/index', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types/basic', 'gcli/types/javascript', 'gcli/types/node', 'gcli/cli', 'gcli/commands/help', 'gcli/ui/console'], function(require, exports, module) {
 
   // The API for use by command authors
   exports.addCommand = require('gcli/canon').addCommand;
   exports.removeCommand = require('gcli/canon').removeCommand;
   exports.lookup = mozl10n.lookup;
   exports.lookupFormat = mozl10n.lookupFormat;
 
   // Internal startup process. Not exported
   require('gcli/types/basic').startup();
   require('gcli/types/javascript').startup();
   require('gcli/types/node').startup();
   require('gcli/cli').startup();
+  require('gcli/commands/help').startup();
 
   var Requisition = require('gcli/cli').Requisition;
-  var Display = require('gcli/ui/display').Display;
+  var Console = require('gcli/ui/console').Console;
 
   var cli = require('gcli/cli');
   var jstype = require('gcli/types/javascript');
   var nodetype = require('gcli/types/node');
 
   /**
    * API for use by HUDService only.
    * This code is internal and subject to change without notice.
@@ -734,25 +735,25 @@ define('gcli/index', ['require', 'export
       jstype.setGlobalObject(opts.jsEnvironment.globalObject);
       nodetype.setDocument(opts.contentDocument);
       cli.setEvalFunction(opts.jsEnvironment.evalFunction);
 
       if (opts.requisition == null) {
         opts.requisition = new Requisition(opts.environment, opts.chromeDocument);
       }
 
-      opts.display = new Display(opts);
+      opts.console = new Console(opts);
     },
 
     /**
      * Undo the effects of createView() to prevent memory leaks
      */
     removeView: function(opts) {
-      opts.display.destroy();
-      delete opts.display;
+      opts.console.destroy();
+      delete opts.console;
 
       opts.requisition.destroy();
       delete opts.requisition;
 
       cli.unsetEvalFunction();
       nodetype.unsetDocument();
       jstype.unsetGlobalObject();
     },
@@ -1024,17 +1025,18 @@ canon.removeCommand = function removeCom
   canon.canonChange();
 };
 
 /**
  * Retrieve a command by name
  * @param name The name of the command to retrieve
  */
 canon.getCommand = function getCommand(name) {
-  return commands[name];
+  // '|| undefined' is to silence 'reference to undefined property' warnings
+  return commands[name] || undefined;
 };
 
 /**
  * Get an array of all the registered commands.
  */
 canon.getCommands = function getCommands() {
   // return Object.values(commands);
   return Object.keys(commands).map(function(name) {
@@ -1185,19 +1187,27 @@ exports.createEvent = function(name) {
   return event;
 };
 
 
 //------------------------------------------------------------------------------
 
 var dom = {};
 
+/**
+ * XHTML namespace
+ */
 dom.NS_XHTML = 'http://www.w3.org/1999/xhtml';
 
 /**
+ * XUL namespace
+ */
+dom.NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+
+/**
  * Create an HTML or XHTML element depending on whether the document is HTML
  * or XML based. Where HTML/XHTML elements are distinguished by whether they
  * are created using doc.createElementNS('http://www.w3.org/1999/xhtml', tag)
  * or doc.createElement(tag)
  * If you want to create a XUL element then you don't have a problem knowing
  * what namespace you want.
  * @param doc The document in which to create the element
  * @param tag The name of the tag to create
@@ -1241,34 +1251,40 @@ dom.importCss = function(cssText, doc) {
 };
 
 /**
  * There are problems with innerHTML on XML documents, so we need to do a dance
  * using document.createRange().createContextualFragment() when in XML mode
  */
 dom.setInnerHtml = function(elem, html) {
   if (dom.isXmlDocument(elem.ownerDocument)) {
-    dom.clearElement(elem);
-    html = '<div xmlns="' + dom.NS_XHTML + '">' + html + '</div>';
-    var range = elem.ownerDocument.createRange();
-    var child = range.createContextualFragment(html).childNodes[0];
-    while (child.hasChildNodes()) {
-      elem.appendChild(child.firstChild);
+    try {
+      dom.clearElement(elem);
+      html = '<div xmlns="' + dom.NS_XHTML + '">' + html + '</div>';
+      var range = elem.ownerDocument.createRange();
+      var child = range.createContextualFragment(html).firstChild;
+      while (child.hasChildNodes()) {
+        elem.appendChild(child.firstChild);
+      }
+    }
+    catch (ex) {
+      console.error('Bad XHTML', ex);
+      console.trace();
+      throw ex;
     }
   }
   else {
     elem.innerHTML = html;
   }
 };
 
 /**
- * How to detect if we're in an XUL document (and therefore should create
- * elements in an XHTML namespace)
- * In a Mozilla XUL document, document.xmlVersion = null, however in Chrome
- * document.contentType = undefined.
+ * How to detect if we're in an XML document.
+ * In a Mozilla we check that document.xmlVersion = null, however in Chrome
+ * we use document.contentType = undefined.
  * @param doc The document element to work from (defaulted to the global
  * 'document' if missing
  */
 dom.isXmlDocument = function(doc) {
   doc = doc || document;
   // Best test for Firefox
   if (doc.contentType && doc.contentType != 'text/html') {
     return true;
@@ -1474,16 +1490,23 @@ exports.lookup = function(key) {
     return stringBundle.GetStringFromName(key);
   }
   catch (ex) {
     console.error('Failed to lookup ', key, ex);
     return key;
   }
 };
 
+/** @see propertyLookup in lib/gcli/l10n.js */
+exports.propertyLookup = Proxy.create({
+  get: function(rcvr, name) {
+    return exports.lookup(name);
+  }
+});
+
 /** @see lookupFormat in lib/gcli/l10n.js */
 exports.lookupFormat = function(key, swaps) {
   try {
     return stringBundle.formatStringFromName(key, swaps, swaps.length);
   }
   catch (ex) {
     console.error('Failed to format ', key, ex);
     return key;
@@ -3457,16 +3480,24 @@ exports.setDocument = function(document)
 
 /**
  * Undo the effects of setDocument()
  */
 exports.unsetDocument = function() {
   doc = undefined;
 };
 
+/**
+ * Getter for the document that contains the nodes we're matching
+ * Most for changing things back to how they were for unit testing
+ */
+exports.getDocument = function() {
+  return doc;
+};
+
 
 /**
  * A CSS expression that refers to a single node
  */
 function NodeType(typeSpec) {
   if (typeSpec != null) {
     throw new Error('NodeType can not be customized');
   }
@@ -4037,17 +4068,25 @@ UnassignedAssignment.prototype.setUnassi
  * ExecutionContext.
  * @param doc A DOM Document passed to commands using ExecutionContext in
  * order to allow creation of DOM nodes. If missing Requisition will use the
  * global 'document'.
  * @constructor
  */
 function Requisition(environment, doc) {
   this.environment = environment;
-  this.document = doc || document;
+  this.document = doc;
+  if (this.document == null) {
+    try {
+      this.document = document;
+    }
+    catch (ex) {
+      // Ignore
+    }
+  }
 
   // The command that we are about to execute.
   // @see setCommandConversion()
   this.commandAssignment = new CommandAssignment();
 
   // The object that stores of Assignment objects that we are filling out.
   // The Assignment objects are stored under their param.name for named
   // lookup. Note: We make use of the property of Javascript objects that
@@ -4503,17 +4542,18 @@ Requisition.prototype.exec = function(in
 
   if (!command) {
     return false;
   }
 
   var outputObject = {
     command: command,
     args: args,
-    typed: this.toCanonicalString(),
+    typed: this.toString(),
+    canonical: this.toCanonicalString(),
     completed: false,
     start: new Date()
   };
 
   this.commandOutputManager.sendCommandOutput(outputObject);
 
   var onComplete = (function(output, error) {
     if (visible) {
@@ -4522,17 +4562,17 @@ Requisition.prototype.exec = function(in
       outputObject.error = error;
       outputObject.output = output;
       outputObject.completed = true;
       this.commandOutputManager.sendCommandOutput(outputObject);
     }
   }).bind(this);
 
   try {
-    var context = new ExecutionContext(this.environment, this.document);
+    var context = new ExecutionContext(this);
     var reply = command.exec(args, context);
 
     if (reply != null && reply.isPromise) {
       reply.then(
         function(data) { onComplete(data, false); },
         function(error) { onComplete(error, true); });
 
       // Add progress to our promise and add a handler for it here
@@ -5007,19 +5047,20 @@ Requisition.prototype._assign = function
 };
 
 exports.Requisition = Requisition;
 
 
 /**
  * Functions and data related to the execution of a command
  */
-function ExecutionContext(environment, document) {
-  this.environment = environment;
-  this.document = document;
+function ExecutionContext(requisition) {
+  this.requisition = requisition;
+  this.environment = requisition.environment;
+  this.document = requisition.document;
 }
 
 ExecutionContext.prototype.createPromise = function() {
   return new Promise();
 };
 
 
 });
@@ -5036,28 +5077,296 @@ define('gcli/promise', ['require', 'expo
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
-define('gcli/ui/display', ['require', 'exports', 'module' , 'gcli/ui/inputter', 'gcli/ui/arg_fetch', 'gcli/ui/menu', 'gcli/ui/focus'], function(require, exports, module) {
+define('gcli/commands/help', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/util', 'gcli/l10n', 'gcli/ui/domtemplate', 'text!gcli/commands/help.css', 'text!gcli/commands/help_intro.html', 'text!gcli/commands/help_list.html', 'text!gcli/commands/help_man.html'], function(require, exports, module) {
+var help = exports;
+
+
+var canon = require('gcli/canon');
+var util = require('gcli/util');
+var l10n = require('gcli/l10n');
+var domtemplate = require('gcli/ui/domtemplate');
+
+var helpCss = require('text!gcli/commands/help.css');
+var helpStyle = undefined;
+var helpIntroHtml = require('text!gcli/commands/help_intro.html');
+var helpIntroTemplate = undefined;
+var helpListHtml = require('text!gcli/commands/help_list.html');
+var helpListTemplate = undefined;
+var helpManHtml = require('text!gcli/commands/help_man.html');
+var helpManTemplate = undefined;
+
+/**
+ * 'help' command
+ * We delay definition of helpCommandSpec until help.startup() to ensure that
+ * the l10n strings have been loaded
+ */
+var helpCommandSpec;
+
+/**
+ * Registration and de-registration.
+ */
+help.startup = function() {
+
+  helpCommandSpec = {
+    name: 'help',
+    description: l10n.lookup('helpDesc'),
+    manual: l10n.lookup('helpManual'),
+    params: [
+      {
+        name: 'search',
+        type: 'string',
+        description: l10n.lookup('helpSearchDesc'),
+        manual: l10n.lookup('helpSearchManual'),
+        defaultValue: null
+      }
+    ],
+    returnType: 'html',
+
+    exec: function(args, context) {
+      help.onFirstUseStartup(context.document);
+
+      var match = canon.getCommand(args.search);
+      if (match) {
+        var clone = helpManTemplate.cloneNode(true);
+        domtemplate.template(clone, getManTemplateData(match, context),
+                { allowEval: true, stack: 'help_man.html' });
+        return clone;
+      }
+
+      var parent = util.dom.createElement(context.document, 'div');
+      if (!args.search) {
+        parent.appendChild(helpIntroTemplate.cloneNode(true));
+      }
+      parent.appendChild(helpListTemplate.cloneNode(true));
+      domtemplate.template(parent, getListTemplateData(args, context),
+              { allowEval: true, stack: 'help_intro.html | help_list.html' });
+      return parent;
+    }
+  };
+
+  canon.addCommand(helpCommandSpec);
+};
+
+help.shutdown = function() {
+  canon.removeCommand(helpCommandSpec);
+
+  helpListTemplate = undefined;
+  helpStyle.parentElement.removeChild(helpStyle);
+  helpStyle = undefined;
+};
+
+/**
+ * Called when the command is executed
+ */
+help.onFirstUseStartup = function(document) {
+  if (!helpIntroTemplate) {
+    helpIntroTemplate = util.dom.createElement(document, 'div');
+    util.dom.setInnerHtml(helpIntroTemplate, helpIntroHtml);
+  }
+  if (!helpListTemplate) {
+    helpListTemplate = util.dom.createElement(document, 'div');
+    util.dom.setInnerHtml(helpListTemplate, helpListHtml);
+  }
+  if (!helpManTemplate) {
+    helpManTemplate = util.dom.createElement(document, 'div');
+    util.dom.setInnerHtml(helpManTemplate, helpManHtml);
+  }
+  if (!helpStyle && helpCss != null) {
+    helpStyle = util.dom.importCss(helpCss, document);
+  }
+};
+
+/**
+ * Find an element within the passed element with the class gcli-help-command
+ * and update the requisition to contain this text.
+ */
+function updateCommand(element, context) {
+  context.requisition.update({
+    typed: element.querySelector('.gcli-help-command').textContent
+  });
+}
+
+/**
+ * Find an element within the passed element with the class gcli-help-command
+ * and execute this text.
+ */
+function executeCommand(element, context) {
+  context.requisition.exec({
+    visible: true,
+    typed: element.querySelector('.gcli-help-command').textContent
+  });
+}
+
+/**
+ * Create a block of data suitable to be passed to the help_list.html template
+ */
+function getListTemplateData(args, context) {
+  return {
+    l10n: l10n.propertyLookup,
+    lang: context.document.defaultView.navigator.language,
+
+    onclick: function(ev) {
+      updateCommand(ev.currentTarget, context);
+    },
+
+    ondblclick: function(ev) {
+      executeCommand(ev.currentTarget, context);
+    },
+
+    getHeading: function() {
+      return args.search == null ?
+              'Available Commands:' :
+              'Commands starting with \'' + args.search + '\':';
+    },
+
+    getMatchingCommands: function() {
+      var matching = canon.getCommands().filter(function(command) {
+        if (args.search && command.name.indexOf(args.search) !== 0) {
+          // Filtered out because they don't match the search
+          return false;
+        }
+        if (!args.search && command.name.indexOf(' ') != -1) {
+          // We don't show sub commands with plain 'help'
+          return false;
+        }
+        return true;
+      });
+      matching.sort();
+      return matching;
+    }
+  };
+}
+
+/**
+ * Create a block of data suitable to be passed to the help_man.html template
+ */
+function getManTemplateData(command, context) {
+  return {
+    l10n: l10n.propertyLookup,
+    lang: context.document.defaultView.navigator.language,
+
+    command: command,
+
+    onclick: function(ev) {
+      updateCommand(ev.currentTarget, context);
+    },
+
+    getTypeDescription: function(param) {
+      var input = '';
+      if (param.defaultValue === undefined) {
+        input = 'required';
+      }
+      else if (param.defaultValue === null) {
+        input = 'optional';
+      }
+      else {
+        input = param.defaultValue;
+      }
+      return '(' + param.type.name + ', ' + input + ')';
+    }
+  };
+}
+
+});
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+define('gcli/ui/domtemplate', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+  var obj = {};
+  Components.utils.import('resource:///modules/devtools/Templater.jsm', obj);
+  exports.template = obj.template;
+
+});
+define("text!gcli/commands/help.css", [], void 0);
+define("text!gcli/commands/help_intro.html", [], "\n" +
+  "<h2>${l10n.introHeader}</h2>\n" +
+  "\n" +
+  "<p>\n" +
+  "  <a target=\"_blank\" href=\"https://developer.mozilla.org/AppLinks/WebConsoleHelp?locale=${lang}\">\n" +
+  "    ${l10n.introBody}\n" +
+  "  </a>\n" +
+  "</p>\n" +
+  "");
+
+define("text!gcli/commands/help_list.html", [], "\n" +
+  "<h3>${getHeading()}</h3>\n" +
+  "\n" +
+  "<table>\n" +
+  "  <tr foreach=\"command in ${getMatchingCommands()}\"\n" +
+  "      onclick=\"${onclick}\" ondblclick=\"${ondblclick}\">\n" +
+  "    <th class=\"gcli-help-name\">${command.name}</th>\n" +
+  "    <td class=\"gcli-help-arrow\">&#x2192;</td>\n" +
+  "    <td>\n" +
+  "      ${command.description}\n" +
+  "      <span class=\"gcli-out-shortcut gcli-help-command\">help ${command.name}</span>\n" +
+  "    </td>\n" +
+  "  </tr>\n" +
+  "</table>\n" +
+  "");
+
+define("text!gcli/commands/help_man.html", [], "\n" +
+  "<h3>${command.name}</h3>\n" +
+  "\n" +
+  "<h4 class=\"gcli-help-header\">\n" +
+  "  ${l10n.helpManSynopsis}:\n" +
+  "  <span class=\"gcli-help-synopsis\" onclick=\"${onclick}\">\n" +
+  "    <span class=\"gcli-help-command\">${command.name}</span>\n" +
+  "    <span foreach=\"param in ${command.params}\">\n" +
+  "      ${param.defaultValue !== undefined ? '[' + param.name + ']' : param.name}\n" +
+  "    </span>\n" +
+  "  </span>\n" +
+  "</h4>\n" +
+  "\n" +
+  "<h4 class=\"gcli-help-header\">${l10n.helpManDescription}:</h4>\n" +
+  "\n" +
+  "<p class=\"gcli-help-description\">\n" +
+  "  ${command.manual || command.description}\n" +
+  "</p>\n" +
+  "\n" +
+  "<h4 class=\"gcli-help-header\">${l10n.helpManParameters}:</h4>\n" +
+  "\n" +
+  "<ul class=\"gcli-help-parameter\">\n" +
+  "  <li if=\"${command.params.length === 0}\">${l10n.helpManNone}</li>\n" +
+  "  <li foreach=\"param in ${command.params}\">\n" +
+  "    <tt>${param.name}</tt> ${getTypeDescription(param)}\n" +
+  "    <br/>\n" +
+  "    ${param.manual || param.description}\n" +
+  "  </li>\n" +
+  "</ul>\n" +
+  "");
+
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+define('gcli/ui/console', ['require', 'exports', 'module' , 'gcli/ui/inputter', 'gcli/ui/arg_fetch', 'gcli/ui/menu', 'gcli/ui/focus'], function(require, exports, module) {
 
 var Inputter = require('gcli/ui/inputter').Inputter;
 var ArgFetcher = require('gcli/ui/arg_fetch').ArgFetcher;
 var CommandMenu = require('gcli/ui/menu').CommandMenu;
 var FocusManager = require('gcli/ui/focus').FocusManager;
 
 /**
- * Display is responsible for generating the UI for GCLI, this implementation
+ * Console is responsible for generating the UI for GCLI, this implementation
  * is a special case for use inside Firefox
  */
-function Display(options) {
+function Console(options) {
   this.hintElement = options.hintElement;
   this.gcliTerm = options.gcliTerm;
   this.consoleWrap = options.consoleWrap;
   this.requisition = options.requisition;
 
   // Create a FocusManager for the various parts to register with
   this.focusManager = new FocusManager({ document: options.chromeDocument });
   this.focusManager.onFocus.add(this.gcliTerm.show, this.gcliTerm);
@@ -5092,17 +5401,17 @@ function Display(options) {
   this.resizer = this.resizer.bind(this);
   this.chromeWindow.addEventListener('resize', this.resizer, false);
   this.requisition.commandChange.add(this.resizer, this);
 }
 
 /**
  * Avoid memory leaks
  */
-Display.prototype.destroy = function() {
+Console.prototype.destroy = function() {
   this.chromeWindow.removeEventListener('resize', this.resizer, false);
   delete this.resizer;
   delete this.chromeWindow;
   delete this.consoleWrap;
 
   this.hintElement.removeChild(this.menu.element);
   this.menu.destroy();
   this.hintElement.removeChild(this.argFetcher.element);
@@ -5117,56 +5426,57 @@ Display.prototype.destroy = function() {
 
   delete this.gcliTerm;
   delete this.hintElement;
 };
 
 /**
  * Called on chrome window resize, or on divider slide
  */
-Display.prototype.resizer = function() {
+Console.prototype.resizer = function() {
+  // Bug 705109: There are several numbers hard-coded in this function.
+  // This is simpler than calculating them, but error-prone when the UI setup,
+  // the styling or display settings change.
+
   var parentRect = this.consoleWrap.getBoundingClientRect();
+  // Magic number: 64 is the size of the toolbar above the output area
   var parentHeight = parentRect.bottom - parentRect.top - 64;
 
+  // Magic number: 100 is the size at which we decide the hints are too small
+  // to be useful, so we hide them
   if (parentHeight < 100) {
     this.hintElement.classList.add('gcliterm-hint-nospace');
   }
   else {
     this.hintElement.classList.remove('gcliterm-hint-nospace');
 
     var isMenuVisible = this.menu.element.style.display !== 'none';
     if (isMenuVisible) {
       this.menu.setMaxHeight(parentHeight);
 
-      // Magic numbers. We have 2 options - lots of complex dom math to derive
-      // the height of a menu item (19 pixels) and the vertical padding
-      // (22 pixels), or we could just hard-code. The former is *slightly* more
-      // resilient to refactoring (but still breaks with dom structure changes),
-      // the latter is simpler, faster and easier.
+      // Magic numbers: 19 = height of a menu item, 22 = total vertical padding
+      // of container
       var idealMenuHeight = (19 * this.menu.items.length) + 22;
-
       if (idealMenuHeight > parentHeight) {
-        this.hintElement.style.overflowY = 'scroll';
-        this.hintElement.style.borderBottomColor = 'threedshadow';
+        this.hintElement.classList.add('gcliterm-hint-scroll');
       }
       else {
-        this.hintElement.style.overflowY = null;
-        this.hintElement.style.borderBottomColor = 'white';
+        this.hintElement.classList.remove('gcliterm-hint-scroll');
       }
     }
     else {
       this.argFetcher.setMaxHeight(parentHeight);
 
       this.hintElement.style.overflowY = null;
       this.hintElement.style.borderBottomColor = 'white';
     }
   }
 };
 
-exports.Display = Display;
+exports.Console = Console;
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
@@ -5577,18 +5887,19 @@ cliView.Inputter = Inputter;
  * Completer is an 'input-like' element that sits  an input element annotating
  * it with visual goodness.
  * @param {object} options An object that contains various options which
  * customizes how the completer functions.
  * Properties on the options object:
  * - document (required) DOM document to be used in creating elements
  * - requisition (required) A GCLI Requisition object whose state is monitored
  * - completeElement (optional) An element to use
- * - completionPrompt (optional) The prompt to show before a completion.
- *   Defaults to '&#x00bb;' (double greater-than, a.k.a right guillemet).
+ * - completionPrompt (optional) The prompt - defaults to '\u00bb'
+ *   (double greater-than, a.k.a right guillemet). The prompt is used directly
+ *   in a TextNode, so HTML entities are not allowed.
  */
 function Completer(options) {
   this.document = options.document || document;
   this.requisition = options.requisition;
   this.elementCreated = false;
 
   this.element = options.completeElement || 'gcli-row-complete';
   if (typeof this.element === 'string') {
@@ -5601,17 +5912,17 @@ function Completer(options) {
       this.element.className = 'gcli-in-complete gcli-in-valid';
       this.element.setAttribute('tabindex', '-1');
       this.element.setAttribute('aria-live', 'polite');
     }
   }
 
   this.completionPrompt = typeof options.completionPrompt === 'string'
       ? options.completionPrompt
-      : '&#x00bb;';
+      : '\u00bb';
 
   if (options.inputBackgroundElement) {
     this.backgroundElement = options.inputBackgroundElement;
   }
   else {
     this.backgroundElement = this.element;
   }
 }
@@ -5709,86 +6020,123 @@ function isStrictCompletion(inputValue, 
 
 /**
  * Bring the completion element up to date with what the requisition says
  */
 Completer.prototype.update = function(input) {
   var current = this.requisition.getAssignmentAt(input.cursor.start);
   var predictions = current.getPredictions();
 
-  var completion = '<span class="gcli-prompt">' + this.completionPrompt + '</span> ';
+  dom.clearElement(this.element);
+
+  // All this DOM manipulation is equivalent to the HTML below.
+  // It's not a template because it's very simple except appendMarkupStatus()
+  // which is complex due to a need to merge spans.
+  // Bug 707131 questions if we couldn't simplify this to use a template.
+  //
+  // <span class="gcli-prompt">${completionPrompt}</span>
+  // ${appendMarkupStatus()}
+  // ${prefix}
+  // <span class="gcli-in-ontab">${contents}</span>
+  // <span class="gcli-in-closebrace" if="${unclosedJs}">}<span>
+
+  var document = this.element.ownerDocument;
+  var prompt = document.createElement('span');
+  prompt.classList.add('gcli-prompt');
+  prompt.appendChild(document.createTextNode(this.completionPrompt + ' '));
+  this.element.appendChild(prompt);
+
   if (input.typed.length > 0) {
     var scores = this.requisition.getInputStatusMarkup(input.cursor.start);
-    completion += this.markupStatusScore(scores, input);
+    this.appendMarkupStatus(this.element, scores, input);
   }
 
   if (input.typed.length > 0 && predictions.length > 0) {
     var tab = predictions[0].name;
     var existing = current.getArg().text;
-    if (isStrictCompletion(existing, tab) && input.cursor.start === input.typed.length) {
-      // Display the suffix of the prediction as the completion.
+
+    var contents;
+    var prefix = null;
+
+    if (isStrictCompletion(existing, tab) &&
+            input.cursor.start === input.typed.length) {
+      // Display the suffix of the prediction as the completion
       var numLeadingSpaces = existing.match(/^(\s*)/)[0].length;
-      var suffix = tab.slice(existing.length - numLeadingSpaces);
-      completion += '<span class="gcli-in-ontab">' + suffix + '</span>';
+      contents = tab.slice(existing.length - numLeadingSpaces);
     } else {
       // Display the '-> prediction' at the end of the completer element
-      completion += ' &#xa0;<span class="gcli-in-ontab">&#x21E5; ' +
-          tab + '</span>';
-    }
-  }
-
-  // A hack to add a grey '}' to the end of the command line when we've opened
+      prefix = ' \u00a0';         // aka &nbsp;
+      contents = '\u21E5 ' + tab; // aka &rarr; the right arrow
+    }
+
+    if (prefix != null) {
+      this.element.appendChild(document.createTextNode(prefix));
+    }
+
+    var suffix = document.createElement('span');
+    suffix.classList.add('gcli-in-ontab');
+    suffix.appendChild(document.createTextNode(contents));
+    this.element.appendChild(suffix);
+  }
+
+  // Add a grey '}' to the end of the command line when we've opened
   // with a { but haven't closed it
   var command = this.requisition.commandAssignment.getValue();
-  if (command && command.name === '{') {
-    if (this.requisition.getAssignment(0).getArg().suffix.indexOf('}') === -1) {
-      completion += '<span class="gcli-in-closebrace">}</span>';
-    }
-  }
-
-  dom.setInnerHtml(this.element, completion);
+  var unclosedJs = command && command.name === '{' &&
+          this.requisition.getAssignment(0).getArg().suffix.indexOf('}') === -1;
+  if (unclosedJs) {
+    var close = document.createElement('span');
+    close.classList.add('gcli-in-closebrace');
+    close.appendChild(document.createTextNode('}'));
+    this.element.appendChild(close);
+  }
 };
 
 /**
  * Mark-up an array of Status values with spans
  */
-Completer.prototype.markupStatusScore = function(scores, input) {
-  var completion = '';
+Completer.prototype.appendMarkupStatus = function(element, scores, input) {
   if (scores.length === 0) {
-    return completion;
-  }
-
+    return;
+  }
+
+  var document = element.ownerDocument;
   var i = 0;
   var lastStatus = -1;
+  var span;
+  var contents = '';
+
   while (true) {
     if (lastStatus !== scores[i]) {
       var state = scores[i];
       if (!state) {
         console.error('No state at i=' + i + '. scores.len=' + scores.length);
         state = Status.VALID;
       }
-      completion += '<span class="gcli-in-' + state.toString().toLowerCase() + '">';
+      span = document.createElement('span');
+      span.classList.add('gcli-in-' + state.toString().toLowerCase());
       lastStatus = scores[i];
     }
     var char = input.typed[i];
     if (char === ' ') {
-      char = '&#xa0;';
-    }
-    completion += char;
+      char = '\u00a0';
+    }
+    contents += char;
     i++;
     if (i === input.typed.length) {
-      completion += '</span>';
+      span.appendChild(document.createTextNode(contents));
+      this.element.appendChild(span);
       break;
     }
     if (lastStatus !== scores[i]) {
-      completion += '</span>';
-    }
-  }
-
-  return completion;
+      span.appendChild(document.createTextNode(contents));
+      this.element.appendChild(span);
+      contents = '';
+    }
+  }
 };
 
 cliView.Completer = Completer;
 
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
@@ -5862,17 +6210,17 @@ exports.History = History;
 define('gcli/ui/arg_fetch', ['require', 'exports', 'module' , 'gcli/util', 'gcli/types', 'gcli/ui/field', 'gcli/ui/domtemplate', 'text!gcli/ui/arg_fetch.css', 'text!gcli/ui/arg_fetch.html'], function(require, exports, module) {
 var argFetch = exports;
 
 
 var dom = require('gcli/util').dom;
 var Status = require('gcli/types').Status;
 
 var getField = require('gcli/ui/field').getField;
-var Templater = require('gcli/ui/domtemplate').Templater;
+var domtemplate = require('gcli/ui/domtemplate');
 
 var editorCss = require('text!gcli/ui/arg_fetch.css');
 var argFetchHtml = require('text!gcli/ui/arg_fetch.html');
 
 
 /**
  * A widget to display an inline dialog which allows the user to fill out
  * the arguments to a command.
@@ -5891,17 +6239,16 @@ function ArgFetcher(options) {
     throw new Error('No document');
   }
 
   this.element =  dom.createElement(this.document, 'div');
   this.element.className = options.argFetcherClass || 'gcli-argfetch';
   // We cache the fields we create so we can destroy them later
   this.fields = [];
 
-  this.tmpl = new Templater();
   // Populated by template
   this.okElement = null;
 
   // Pull the HTML into the DOM, but don't add it to the document
   if (editorCss != null) {
     this.style = dom.importCss(editorCss, this.document);
   }
 
@@ -5948,17 +6295,18 @@ ArgFetcher.prototype.onCommandChange = f
       // Just the text has changed
       return;
     }
 
     this.fields.forEach(function(field) { field.destroy(); });
     this.fields = [];
 
     var reqEle = this.reqTempl.cloneNode(true);
-    this.tmpl.processNode(reqEle, this);
+    domtemplate.template(reqEle, this,
+            { allowEval: true, stack: 'arg_fetch.html' });
     dom.clearElement(this.element);
     this.element.appendChild(reqEle);
 
     var status = this.requisition.getStatus();
     this.okElement.disabled = (status === Status.VALID);
 
     this.element.style.display = 'block';
   }
@@ -6003,17 +6351,17 @@ ArgFetcher.prototype.getInputFor = funct
 
     // Bug 681894: we add the field as a property of the assignment so that
     // #linkMessageElement() can call 'field.setMessageElement(element)'
     assignment.field = newField;
 
     return newField.element;
   }
   catch (ex) {
-    // This is called from within Templater which can make tracing errors hard
+    // This is called from within template() which can make tracing errors hard
     // so we log here if anything goes wrong
     console.error(ex);
     return '';
   }
 };
 
 /**
  * Called by the template to setup an mutable message field
@@ -6247,17 +6595,17 @@ exports.getField = getField;
  */
 function StringField(type, options) {
   this.document = options.document;
   this.type = type;
   this.arg = new Argument();
 
   this.element = dom.createElement(this.document, 'input');
   this.element.type = 'text';
-  this.element.className = 'gcli-field';
+  this.element.classList.add('gcli-field');
 
   this.onInputChange = this.onInputChange.bind(this);
   this.element.addEventListener('keyup', this.onInputChange, false);
 
   this.fieldChanged = createEvent('StringField.fieldChanged');
 }
 
 StringField.prototype = Object.create(Field.prototype);
@@ -6407,17 +6755,17 @@ addField(BooleanField);
  * </ul>
  */
 function SelectionField(type, options) {
   this.document = options.document;
   this.type = type;
   this.items = [];
 
   this.element = dom.createElement(this.document, 'select');
-  this.element.className = 'gcli-field';
+  this.element.classList.add('gcli-field');
   this._addOption({
     name: l10n.lookupFormat('fieldSelectionSelect', [ options.name ])
   });
   var lookup = this.type.getLookup();
   lookup.forEach(this._addOption, this);
 
   this.onInputChange = this.onInputChange.bind(this);
   this.element.addEventListener('change', this.onInputChange, false);
@@ -6482,18 +6830,18 @@ function JavascriptField(type, options) 
   this.onInputChange = this.onInputChange.bind(this);
   this.arg = new Argument('', '{ ', ' }');
 
   this.element = dom.createElement(this.document, 'div');
 
   this.input = dom.createElement(this.document, 'input');
   this.input.type = 'text';
   this.input.addEventListener('keyup', this.onInputChange, false);
-  this.input.style.marginBottom = '0';
-  this.input.className = 'gcli-field';
+  this.input.classList.add('gcli-field');
+  this.input.classList.add('gcli-field-javascript');
   this.element.appendChild(this.input);
 
   this.menu = new Menu({ document: this.document, field: true });
   this.element.appendChild(this.menu.element);
 
   this.setConversion(this.type.parse(new Argument('')));
 
   this.fieldChanged = createEvent('JavascriptField.fieldChanged');
@@ -6675,28 +7023,28 @@ function ArrayField(type, options) {
   this.options = options;
   this.requ = options.requisition;
 
   this._onAdd = this._onAdd.bind(this);
   this.members = [];
 
   // <div class=gcliArrayParent save="${element}">
   this.element = dom.createElement(this.document, 'div');
-  this.element.className = 'gcliArrayParent';
+  this.element.classList.add('gcli-array-parent');
 
   // <button class=gcliArrayMbrAdd onclick="${_onAdd}" save="${addButton}">Add
   this.addButton = dom.createElement(this.document, 'button');
-  this.addButton.className = 'gcliArrayMbrAdd';
+  this.addButton.classList.add('gcli-array-member-add');
   this.addButton.addEventListener('click', this._onAdd, false);
   this.addButton.innerHTML = l10n.lookup('fieldArrayAdd');
   this.element.appendChild(this.addButton);
 
   // <div class=gcliArrayMbrs save="${mbrElement}">
   this.container = dom.createElement(this.document, 'div');
-  this.container.className = 'gcliArrayMbrs';
+  this.container.classList.add('gcli-array-members');
   this.element.appendChild(this.container);
 
   this.onInputChange = this.onInputChange.bind(this);
 
   this.fieldChanged = createEvent('ArrayField.fieldChanged');
 }
 
 ArrayField.prototype = Object.create(Field.prototype);
@@ -6729,17 +7077,17 @@ ArrayField.prototype.getConversion = fun
     arrayArg.addArgument(conversion.arg);
   }
   return new ArrayConversion(conversions, arrayArg);
 };
 
 ArrayField.prototype._onAdd = function(ev, subConversion) {
   // <div class=gcliArrayMbr save="${element}">
   var element = dom.createElement(this.document, 'div');
-  element.className = 'gcliArrayMbr';
+  element.classList.add('gcli-array-member');
   this.container.appendChild(element);
 
   // ${field.element}
   var field = getField(this.type.subtype, this.options);
   field.fieldChanged.add(function() {
     var conversion = this.getConversion();
     this.fieldChanged({ conversion: conversion });
     this.setMessage(conversion.message);
@@ -6747,17 +7095,17 @@ ArrayField.prototype._onAdd = function(e
 
   if (subConversion) {
     field.setConversion(subConversion);
   }
   element.appendChild(field.element);
 
   // <div class=gcliArrayMbrDel onclick="${_onDel}">
   var delButton = dom.createElement(this.document, 'button');
-  delButton.className = 'gcliArrayMbrDel';
+  delButton.classList.add('gcli-array-member-del');
   delButton.addEventListener('click', this._onDel, false);
   delButton.innerHTML = l10n.lookup('fieldArrayDel');
   element.appendChild(delButton);
 
   var member = {
     element: element,
     field: field,
     parent: this
@@ -6789,17 +7137,17 @@ define('gcli/ui/menu', ['require', 'expo
 
 
 var dom = require('gcli/util').dom;
 
 var Conversion = require('gcli/types').Conversion;
 var Argument = require('gcli/argument').Argument;
 var canon = require('gcli/canon');
 
-var Templater = require('gcli/ui/domtemplate').Templater;
+var domtemplate = require('gcli/ui/domtemplate');
 
 var menuCss = require('text!gcli/ui/menu.css');
 var menuHtml = require('text!gcli/ui/menu.html');
 
 
 /**
  * Menu is a display of the commands that are possible given the state of a
  * requisition.
@@ -6872,17 +7220,17 @@ Menu.prototype.show = function(items, er
   this.items = items;
 
   if (this.error == null && this.items.length === 0) {
     this.element.style.display = 'none';
     return;
   }
 
   var options = this.optTempl.cloneNode(true);
-  new Templater().processNode(options, this);
+  domtemplate.template(options, this, { allowEval: true, stack: 'menu.html' });
 
   dom.clearElement(this.element);
   this.element.appendChild(options);
 
   this.element.style.display = 'block';
 };
 
 /**
@@ -6980,28 +7328,16 @@ CommandMenu.prototype.onCommandChange = 
     this.hide();
   }
 };
 
 exports.CommandMenu = CommandMenu;
 
 
 });
-/*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
- */
-
-define('gcli/ui/domtemplate', ['require', 'exports', 'module' ], function(require, exports, module) {
-
-  Components.utils.import("resource:///modules/devtools/Templater.jsm");
-  exports.Templater = Templater;
-
-});
 define("text!gcli/ui/menu.css", [], void 0);
 define("text!gcli/ui/menu.html", [], "\n" +
   "<table class=\"gcli-menu-template\" aria-live=\"polite\">\n" +
   "  <tr class=\"gcli-menu-option\" foreach=\"item in ${items}\"\n" +
   "      onclick=\"${onItemClick}\" title=\"${item.manual || ''}\">\n" +
   "    <td class=\"gcli-menu-name\">${item.name}</td>\n" +
   "    <td class=\"gcli-menu-desc\">${item.description}</td>\n" +
   "  </tr>\n" +
--- a/browser/devtools/webconsole/test/browser/browser_gcli_integrate.js
+++ b/browser/devtools/webconsole/test/browser/browser_gcli_integrate.js
@@ -78,22 +78,22 @@ function testCallCommands() {
 
   // Test unsuccessful auto-completion
   gcliterm.inputNode.value = "ec";
   gcliterm.inputNode.focus();
   EventUtils.synthesizeKey("d", {});
   is(gcliterm.completeNode.textContent, " ecd", "Completion for \"ecd\"");
 
   // Test a normal command's life cycle
-  gcliterm.opts.display.inputter.setInput("echo hello world");
+  gcliterm.opts.console.inputter.setInput("echo hello world");
   gcliterm.opts.requisition.exec();
 
-  let nodes = hud.outputNode.querySelectorAll("description");
+  let nodes = hud.outputNode.querySelectorAll(".gcliterm-msg-body");
 
-  is(nodes.length, 2, "Right number of output nodes");
+  is(nodes.length, 1, "Right number of output nodes");
   ok(/hello world/.test(nodes[0].textContent), "the command's output is correct.");
 
   gcliterm.clearOutput();
 }
 
 function testRemoveCommands() {
   let gcli = require("gcli/index");
   gcli.removeCommand(tselarr);
--- a/browser/devtools/webconsole/test/browser/browser_gcli_web.js
+++ b/browser/devtools/webconsole/test/browser/browser_gcli_web.js
@@ -49,34 +49,38 @@ var define = obj.gcli._internal.define;
 var console = obj.gcli._internal.console;
 var Node = Components.interfaces.nsIDOMNode;
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
-define('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testTokenize', 'gclitest/testSplit', 'gclitest/testCli', 'gclitest/testHistory', 'gclitest/testRequire', 'gclitest/testJs'], function(require, exports, module) {
+define('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testTokenize', 'gclitest/testSplit', 'gclitest/testCli', 'gclitest/testExec', 'gclitest/testKeyboard', 'gclitest/testHistory', 'gclitest/testRequire', 'gclitest/testJs'], function(require, exports, module) {
 
   // We need to make sure GCLI is initialized before we begin testing it
   require('gcli/index');
 
   var examiner = require('test/examiner');
 
   // It's tempting to want to unify these strings and make addSuite() do the
   // call to require(), however that breaks the build system which looks for
   // the strings passed to require
   examiner.addSuite('gclitest/testTokenize', require('gclitest/testTokenize'));
   examiner.addSuite('gclitest/testSplit', require('gclitest/testSplit'));
   examiner.addSuite('gclitest/testCli', require('gclitest/testCli'));
+  examiner.addSuite('gclitest/testExec', require('gclitest/testExec'));
+  examiner.addSuite('gclitest/testKeyboard', require('gclitest/testKeyboard'));
   examiner.addSuite('gclitest/testHistory', require('gclitest/testHistory'));
   examiner.addSuite('gclitest/testRequire', require('gclitest/testRequire'));
   examiner.addSuite('gclitest/testJs', require('gclitest/testJs'));
 
   examiner.run();
+  console.log('Completed test suite');
+  // examiner.log();
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
@@ -163,16 +167,29 @@ examiner.toRemote = function() {
   return {
     suites: Object.keys(examiner.suites).map(function(suiteName) {
       return examiner.suites[suiteName].toRemote();
     }.bind(this))
   };
 };
 
 /**
+ * Output a test summary to console.log
+ */
+examiner.log = function() {
+  var remote = this.toRemote();
+  remote.suites.forEach(function(suite) {
+    console.log(suite.name);
+    suite.tests.forEach(function(test) {
+      console.log('- ' + test.name, test.status.name, test.message || '');
+    });
+  });
+};
+
+/**
  * Used by assert to record a failure against the current test
  */
 examiner.recordError = function(message) {
   if (!currentTest) {
     console.error('No currentTest for ' + message);
     return;
   }
 
@@ -294,18 +311,18 @@ Test.prototype.run = function() {
 
   try {
     this.func.apply(this.suite);
   }
   catch (ex) {
     this.status = stati.fail;
     this.messages.push('' + ex);
     console.error(ex);
-    if (console.trace) {
-      console.trace();
+    if (ex.stack) {
+      console.error(ex.stack);
     }
   }
 
   if (this.status === stati.executing) {
     this.status = stati.pass;
   }
 
   currentTest = null;
@@ -698,21 +715,22 @@ exports.testJavascript = function() {
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
-define('gclitest/commands', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types/basic', 'gcli/types'], function(require, exports, module) {
+define('gclitest/commands', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/util', 'gcli/types/basic', 'gcli/types'], function(require, exports, module) {
 var commands = exports;
 
 
 var canon = require('gcli/canon');
+var util = require('gcli/util');
 
 var SelectionType = require('gcli/types/basic').SelectionType;
 var DeferredType = require('gcli/types/basic').DeferredType;
 var types = require('gcli/types');
 
 /**
  * Registration and de-registration.
  */
@@ -720,47 +738,57 @@ commands.setup = function() {
   commands.option1.type = types.getType('number');
   commands.option2.type = types.getType('boolean');
 
   types.registerType(commands.optionType);
   types.registerType(commands.optionValue);
 
   canon.addCommand(commands.tsv);
   canon.addCommand(commands.tsr);
+  canon.addCommand(commands.tse);
+  canon.addCommand(commands.tsj);
+  canon.addCommand(commands.tsb);
+  canon.addCommand(commands.tss);
   canon.addCommand(commands.tsu);
   canon.addCommand(commands.tsn);
   canon.addCommand(commands.tsnDif);
   canon.addCommand(commands.tsnExt);
   canon.addCommand(commands.tsnExte);
   canon.addCommand(commands.tsnExten);
   canon.addCommand(commands.tsnExtend);
   canon.addCommand(commands.tselarr);
   canon.addCommand(commands.tsm);
+  canon.addCommand(commands.tsg);
 };
 
 commands.shutdown = function() {
   canon.removeCommand(commands.tsv);
   canon.removeCommand(commands.tsr);
+  canon.removeCommand(commands.tse);
+  canon.removeCommand(commands.tsj);
+  canon.removeCommand(commands.tsb);
+  canon.removeCommand(commands.tss);
   canon.removeCommand(commands.tsu);
   canon.removeCommand(commands.tsn);
   canon.removeCommand(commands.tsnDif);
   canon.removeCommand(commands.tsnExt);
   canon.removeCommand(commands.tsnExte);
   canon.removeCommand(commands.tsnExten);
   canon.removeCommand(commands.tsnExtend);
   canon.removeCommand(commands.tselarr);
   canon.removeCommand(commands.tsm);
+  canon.removeCommand(commands.tsg);
 
   types.deregisterType(commands.optionType);
   types.deregisterType(commands.optionValue);
 };
 
 
-commands.option1 = { };
-commands.option2 = { };
+commands.option1 = { type: types.getType('string') };
+commands.option2 = { type: types.getType('number') };
 
 commands.optionType = new SelectionType({
   name: 'optionType',
   lookup: [
     { name: 'option1', value: commands.option1 },
     { name: 'option2', value: commands.option2 }
   ],
   noMatch: function() {
@@ -784,90 +812,151 @@ commands.optionValue = new DeferredType(
       return commands.optionType.lastOption.type;
     }
     else {
       return types.getType('blank');
     }
   }
 });
 
+commands.commandExec = util.createEvent('commands.commandExec');
+
+function createExec(name) {
+  return function(args, context) {
+    var data = {
+      command: commands[name],
+      args: args,
+      context: context
+    };
+    commands.commandExec(data);
+    return data;
+  };
+}
+
 commands.tsv = {
   name: 'tsv',
   params: [
     { name: 'optionType', type: 'optionType' },
     { name: 'optionValue', type: 'optionValue' }
   ],
-  exec: function(args, context) { }
+  exec: createExec('tsv')
 };
 
 commands.tsr = {
   name: 'tsr',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(args, context) { }
+  exec: createExec('tsr')
+};
+
+commands.tse = {
+  name: 'tse',
+  params: [ { name: 'node', type: 'node' } ],
+  exec: createExec('tse')
+};
+
+commands.tsj = {
+  name: 'tsj',
+  params: [ { name: 'javascript', type: 'javascript' } ],
+  exec: createExec('tsj')
+};
+
+commands.tsb = {
+  name: 'tsb',
+  params: [ { name: 'toggle', type: 'boolean' } ],
+  exec: createExec('tsb')
+};
+
+commands.tss = {
+  name: 'tss',
+  exec: createExec('tss')
 };
 
 commands.tsu = {
   name: 'tsu',
-  params: [ { name: 'num', type: 'number' } ],
-  exec: function(args, context) { }
+  params: [ { name: 'num', type: { name: 'number', max: 10, min: -5, step: 3 } } ],
+  exec: createExec('tsu')
 };
 
 commands.tsn = {
   name: 'tsn'
 };
 
 commands.tsnDif = {
   name: 'tsn dif',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(text) { }
+  exec: createExec('tsnDif')
 };
 
 commands.tsnExt = {
   name: 'tsn ext',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(text) { }
+  exec: createExec('tsnExt')
 };
 
 commands.tsnExte = {
   name: 'tsn exte',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(text) { }
+  exec: createExec('')
 };
 
 commands.tsnExten = {
   name: 'tsn exten',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(text) { }
+  exec: createExec('tsnExte')
 };
 
 commands.tsnExtend = {
   name: 'tsn extend',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(text) { }
+  exec: createExec('tsnExtend')
 };
 
 commands.tselarr = {
   name: 'tselarr',
   params: [
     { name: 'num', type: { name: 'selection', data: [ '1', '2', '3' ] } },
     { name: 'arr', type: { name: 'array', subtype: 'string' } },
   ],
-  exec: function(args, context) {}
+  exec: createExec('tselarr')
 };
 
 commands.tsm = {
   name: 'tsm',
   hidden: true,
   description: 'a 3-param test selection|string|number',
   params: [
     { name: 'abc', type: { name: 'selection', data: [ 'a', 'b', 'c' ] } },
     { name: 'txt', type: 'string' },
     { name: 'num', type: { name: 'number', max: 42, min: 0 } },
   ],
-  exec: function(args, context) {}
+  exec: createExec('tsm')
+};
+
+commands.tsg = {
+  name: 'tsg',
+  hidden: true,
+  description: 'a param group test',
+  params: [
+    { name: 'solo', type: { name: 'selection', data: [ 'aaa', 'bbb', 'ccc' ] } },
+    {
+      group: 'First',
+      params: [
+        { name: 'txt1', type: 'string', defaultValue: null },
+        { name: 'boolean1', type: 'boolean' }
+      ]
+    },
+    {
+      group: 'Second',
+      params: [
+        { name: 'txt2', type: 'string', defaultValue: 'd' },
+        { name: 'num2', type: { name: 'number', defaultValue: 42 } }
+      ]
+    }
+  ],
+  exec: createExec('tsg')
 };
 
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
@@ -1213,16 +1302,288 @@ exports.testNestedCommand = function() {
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
+define('gclitest/testExec', ['require', 'exports', 'module' , 'gcli/cli', 'gcli/types', 'gcli/canon', 'gclitest/commands', 'gcli/types/node', 'test/assert'], function(require, exports, module) {
+
+
+var Requisition = require('gcli/cli').Requisition;
+var Status = require('gcli/types').Status;
+var canon = require('gcli/canon');
+var commands = require('gclitest/commands');
+var nodetype = require('gcli/types/node');
+
+var test = require('test/assert');
+
+var actualExec;
+var actualOutput;
+
+exports.setup = function() {
+  commands.setup();
+  commands.commandExec.add(onCommandExec);
+  canon.commandOutputManager.addListener(onCommandOutput);
+};
+
+exports.shutdown = function() {
+  commands.shutdown();
+  commands.commandExec.remove(onCommandExec);
+  canon.commandOutputManager.removeListener(onCommandOutput);
+};
+
+function onCommandExec(ev) {
+  actualExec = ev;
+}
+
+function onCommandOutput(ev) {
+  actualOutput = ev.output;
+}
+
+function exec(command, expectedArgs) {
+  var environment = {};
+
+  var requisition = new Requisition(environment);
+  var reply = requisition.exec({ typed: command });
+
+  test.is(command.indexOf(actualExec.command.name), 0, 'Command name: ' + command);
+
+  if (reply !== true) {
+    test.ok(false, 'reply = false for command: ' + command);
+  }
+
+  if (expectedArgs == null) {
+    test.ok(false, 'expectedArgs == null for ' + command);
+    return;
+  }
+  if (actualExec.args == null) {
+    test.ok(false, 'actualExec.args == null for ' + command);
+    return;
+  }
+
+  test.is(Object.keys(expectedArgs).length, Object.keys(actualExec.args).length,
+          'Arg count: ' + command);
+  Object.keys(expectedArgs).forEach(function(arg) {
+    var expectedArg = expectedArgs[arg];
+    var actualArg = actualExec.args[arg];
+
+    if (Array.isArray(expectedArg)) {
+      if (!Array.isArray(actualArg)) {
+        test.ok(false, 'actual is not an array. ' + command + '/' + arg);
+        return;
+      }
+
+      test.is(expectedArg.length, actualArg.length,
+              'Array length: ' + command + '/' + arg);
+      for (var i = 0; i < expectedArg.length; i++) {
+        test.is(expectedArg[i], actualArg[i],
+                'Member: "' + command + '/' + arg + '/' + i);
+      }
+    }
+    else {
+      test.is(expectedArg, actualArg, 'Command: "' + command + '" arg: ' + arg);
+    }
+  });
+
+  test.is(environment, actualExec.context.environment, 'Environment');
+
+  test.is(false, actualOutput.error, 'output error is false');
+  test.is(command, actualOutput.typed, 'command is typed');
+  test.ok(typeof actualOutput.canonical === 'string', 'canonical exists');
+
+  test.is(actualExec.args, actualOutput.args, 'actualExec.args is actualOutput.args');
+}
+
+
+exports.testExec = function() {
+  exec('tss', {});
+
+  // Bug 707008 - GCLI defered types don't work properly
+  // exec('tsv option1 10', { optionType: commands.option1, optionValue: '10' });
+  // exec('tsv option2 10', { optionType: commands.option1, optionValue: 10 });
+
+  exec('tsr fred', { text: 'fred' });
+  exec('tsr fred bloggs', { text: 'fred bloggs' });
+  exec('tsr "fred bloggs"', { text: 'fred bloggs' });
+
+  exec('tsb', { toggle: false });
+  exec('tsb --toggle', { toggle: true });
+
+  exec('tsu 10', { num: 10 });
+  exec('tsu --num 10', { num: 10 });
+
+  // Bug 704829 - Enable GCLI Javascript parameters
+  // The answer to this should be 2
+  exec('tsj { 1 + 1 }', { javascript: '1 + 1' });
+
+  var origDoc = nodetype.getDocument();
+  nodetype.setDocument(mockDoc);
+  exec('tse :root', { node: mockBody });
+  nodetype.setDocument(origDoc);
+
+  exec('tsn dif fred', { text: 'fred' });
+  exec('tsn exten fred', { text: 'fred' });
+  exec('tsn extend fred', { text: 'fred' });
+
+  exec('tselarr 1', { num: '1', arr: [ ] });
+  exec('tselarr 1 a', { num: '1', arr: [ 'a' ] });
+  exec('tselarr 1 a b', { num: '1', arr: [ 'a', 'b' ] });
+
+  exec('tsm a 10 10', { abc: 'a', txt: '10', num: 10 });
+
+  // Bug 707009 - GCLI doesn't always fill in default parameters properly
+  // exec('tsg a', { solo: 'a', txt1: null, boolean1: false, txt2: 'd', num2: 42 });
+};
+
+var mockBody = {
+  style: {}
+};
+
+var mockDoc = {
+  querySelectorAll: function(css) {
+    if (css === ':root') {
+      return {
+        length: 1,
+        item: function(i) {
+          return mockBody;
+        }
+      };
+    }
+    throw new Error('mockDoc.querySelectorAll(\'' + css + '\') error');
+  }
+};
+
+
+});
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+define('gclitest/testKeyboard', ['require', 'exports', 'module' , 'gcli/cli', 'gcli/types', 'gcli/canon', 'gclitest/commands', 'gcli/types/node', 'test/assert'], function(require, exports, module) {
+
+
+var Requisition = require('gcli/cli').Requisition;
+var Status = require('gcli/types').Status;
+var canon = require('gcli/canon');
+var commands = require('gclitest/commands');
+var nodetype = require('gcli/types/node');
+
+var test = require('test/assert');
+
+
+exports.setup = function() {
+  commands.setup();
+};
+
+exports.shutdown = function() {
+  commands.shutdown();
+};
+
+var COMPLETES_TO = 'complete';
+var KEY_UPS_TO = 'keyup';
+var KEY_DOWNS_TO = 'keydown';
+
+function check(initial, action, after) {
+  var requisition = new Requisition();
+  requisition.update({
+    typed: initial,
+    cursor: { start: initial.length, end: initial.length }
+  });
+  var assignment = requisition.getAssignmentAt(initial.length);
+  switch (action) {
+    case COMPLETES_TO:
+      assignment.complete();
+      break;
+
+    case KEY_UPS_TO:
+      assignment.increment();
+      break;
+
+    case KEY_DOWNS_TO:
+      assignment.decrement();
+      break;
+  }
+
+  test.is(after, requisition.toString(), initial + ' + ' + action + ' -> ' + after);
+}
+
+exports.testComplete = function() {
+  check('tsela', COMPLETES_TO, 'tselarr ');
+  check('tsn di', COMPLETES_TO, 'tsn dif ');
+  check('tsg a', COMPLETES_TO, 'tsg aaa ');
+
+  check('{ wind', COMPLETES_TO, '{ window');
+  check('{ window.docum', COMPLETES_TO, '{ window.document');
+  check('{ window.document.titl', COMPLETES_TO, '{ window.document.title ');
+};
+
+exports.testIncrDecr = function() {
+  check('tsu -70', KEY_UPS_TO, 'tsu -5');
+  check('tsu -7', KEY_UPS_TO, 'tsu -5');
+  check('tsu -6', KEY_UPS_TO, 'tsu -5');
+  check('tsu -5', KEY_UPS_TO, 'tsu -3');
+  check('tsu -4', KEY_UPS_TO, 'tsu -3');
+  check('tsu -3', KEY_UPS_TO, 'tsu 0');
+  check('tsu -2', KEY_UPS_TO, 'tsu 0');
+  check('tsu -1', KEY_UPS_TO, 'tsu 0');
+  check('tsu 0', KEY_UPS_TO, 'tsu 3');
+  check('tsu 1', KEY_UPS_TO, 'tsu 3');
+  check('tsu 2', KEY_UPS_TO, 'tsu 3');
+  check('tsu 3', KEY_UPS_TO, 'tsu 6');
+  check('tsu 4', KEY_UPS_TO, 'tsu 6');
+  check('tsu 5', KEY_UPS_TO, 'tsu 6');
+  check('tsu 6', KEY_UPS_TO, 'tsu 9');
+  check('tsu 7', KEY_UPS_TO, 'tsu 9');
+  check('tsu 8', KEY_UPS_TO, 'tsu 9');
+  check('tsu 9', KEY_UPS_TO, 'tsu 10');
+  check('tsu 10', KEY_UPS_TO, 'tsu 10');
+  check('tsu 100', KEY_UPS_TO, 'tsu -5');
+
+  check('tsu -70', KEY_DOWNS_TO, 'tsu 10');
+  check('tsu -7', KEY_DOWNS_TO, 'tsu 10');
+  check('tsu -6', KEY_DOWNS_TO, 'tsu 10');
+  check('tsu -5', KEY_DOWNS_TO, 'tsu -5');
+  check('tsu -4', KEY_DOWNS_TO, 'tsu -5');
+  check('tsu -3', KEY_DOWNS_TO, 'tsu -5');
+  check('tsu -2', KEY_DOWNS_TO, 'tsu -3');
+  check('tsu -1', KEY_DOWNS_TO, 'tsu -3');
+  check('tsu 0', KEY_DOWNS_TO, 'tsu -3');
+  check('tsu 1', KEY_DOWNS_TO, 'tsu 0');
+  check('tsu 2', KEY_DOWNS_TO, 'tsu 0');
+  check('tsu 3', KEY_DOWNS_TO, 'tsu 0');
+  check('tsu 4', KEY_DOWNS_TO, 'tsu 3');
+  check('tsu 5', KEY_DOWNS_TO, 'tsu 3');
+  check('tsu 6', KEY_DOWNS_TO, 'tsu 3');
+  check('tsu 7', KEY_DOWNS_TO, 'tsu 6');
+  check('tsu 8', KEY_DOWNS_TO, 'tsu 6');
+  check('tsu 9', KEY_DOWNS_TO, 'tsu 6');
+  check('tsu 10', KEY_DOWNS_TO, 'tsu 9');
+  check('tsu 100', KEY_DOWNS_TO, 'tsu 10');
+
+  // Bug 707007 - GCLI increment and decrement operations cycle through
+  // selection options in the wrong order
+  check('tselarr 1', KEY_DOWNS_TO, 'tselarr 2');
+  check('tselarr 2', KEY_DOWNS_TO, 'tselarr 3');
+  check('tselarr 3', KEY_DOWNS_TO, 'tselarr 1');
+
+  check('tselarr 3', KEY_UPS_TO, 'tselarr 2');
+};
+
+});
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
 define('gclitest/testHistory', ['require', 'exports', 'module' , 'test/assert', 'gcli/history'], function(require, exports, module) {
 
 var test = require('test/assert');
 var History = require('gcli/history').History;
 
 exports.setup = function() {
 };
 
@@ -1539,28 +1900,32 @@ exports.testBasic = function() {
 function undefine() {
   delete define.modules['gclitest/suite'];
   delete define.modules['test/examiner'];
   delete define.modules['gclitest/testTokenize'];
   delete define.modules['test/assert'];
   delete define.modules['gclitest/testSplit'];
   delete define.modules['gclitest/commands'];
   delete define.modules['gclitest/testCli'];
+  delete define.modules['gclitest/testExec'];
+  delete define.modules['gclitest/testKeyboard'];
   delete define.modules['gclitest/testHistory'];
   delete define.modules['gclitest/testRequire'];
   delete define.modules['gclitest/requirable'];
   delete define.modules['gclitest/testJs'];
 
   delete define.globalDomain.modules['gclitest/suite'];
   delete define.globalDomain.modules['test/examiner'];
   delete define.globalDomain.modules['gclitest/testTokenize'];
   delete define.globalDomain.modules['test/assert'];
   delete define.globalDomain.modules['gclitest/testSplit'];
   delete define.globalDomain.modules['gclitest/commands'];
   delete define.globalDomain.modules['gclitest/testCli'];
+  delete define.globalDomain.modules['gclitest/testExec'];
+  delete define.globalDomain.modules['gclitest/testKeyboard'];
   delete define.globalDomain.modules['gclitest/testHistory'];
   delete define.globalDomain.modules['gclitest/testRequire'];
   delete define.globalDomain.modules['gclitest/requirable'];
   delete define.globalDomain.modules['gclitest/testJs'];
 }
 
 registerCleanupFunction(function() {
   Services.prefs.clearUserPref("devtools.gcli.enable");
--- a/browser/locales/en-US/chrome/browser/devtools/gcli.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gcli.properties
@@ -62,22 +62,22 @@ jstypeParseError=Error
 # LOCALIZATION NOTE (typesNumberNan): When the command line is passed a
 # number, however the input string is not a valid number, this error message
 # is displayed.
 typesNumberNan=Can't convert "%S" to a number.
 
 # LOCALIZATION NOTE (typesNumberMax): When the command line is passed a
 # number, but the number is bigger than the largest allowed number, this error
 # message is displayed.
-typesNumberMax=%1$S is greater that maximum allowed: %2$S.
+typesNumberMax=%1$S is greater than maximum allowed: %2$S.
 
 # LOCALIZATION NOTE (typesNumberMin): When the command line is passed a
 # number, but the number is lower than the smallest allowed number, this error
 # message is displayed.
-typesNumberMin=%1$S is smaller that minimum allowed: %2$S.
+typesNumberMin=%1$S is smaller than minimum allowed: %2$S.
 
 # LOCALIZATION NOTE (typesSelectionNomatch): When the command line is passed
 # an option with a limited number of correct values, but the passed value is
 # not one of them, this error message is displayed.
 typesSelectionNomatch=Can't use '%S'.
 
 # LOCALIZATION NOTE (nodeParseSyntax): When the command line is expecting a
 # CSS query string, however the passed string is not valid, this error message
@@ -89,8 +89,55 @@ nodeParseSyntax=Syntax error in CSS quer
 # error message is displayed.
 nodeParseMultiple=Too many matches (%S)
 
 # LOCALIZATION NOTE (nodeParseNone): When the command line is expecting a CSS
 # string that matches a single node, but no nodes match, this error message is
 # displayed.
 nodeParseNone=No matches
 
+# LOCALIZATION NOTE (helpDesc): A very short description of the 'help'
+# command. This string is designed to be shown in a menu alongside the command
+# name, which is why it should be as short as possible. See helpManual for a
+# fuller description of what it does.
+helpDesc=Get help on the available commands
+
+# LOCALIZATION NOTE (helpManual): A fuller description of the 'help' command.
+# Displayed when the user asks for help on what it does.
+helpManual=Provide help either on a specific command (if a search string is provided and an exact match is found) or on the available commands (if a search string is not provided, or if no exact match is found).
+
+# LOCALIZATION NOTE (helpSearchDesc): A very short description of the 'search'
+# parameter to the 'help' command. See helpSearchManual for a fuller
+# description of what it does. This string is designed to be shown in a dialog
+# with restricted space, which is why it should be as short as possible.
+helpSearchDesc=Search string
+
+# LOCALIZATION NOTE (helpSearchManual): A fuller description of the 'search'
+# parameter to the 'help' command. Displayed when the user asks for help on
+# what it does.
+helpSearchManual=A search string to use in narrowing down the list of commands that are displayed to the user. Any part of the string can match, regular expressions are not supported.
+
+# LOCALIZATION NOTE (helpManSynopsis): A heading shown at the top of a help
+# page for a command in the console It labels a summary of the parameters to
+# the command
+helpManSynopsis=Synopsis
+
+# LOCALIZATION NOTE (helpManDescription): A heading shown in a help page for a
+# command in the console. This heading precedes the top level description.
+helpManDescription=Description
+
+# LOCALIZATION NOTE (helpManParameters): A heading shown above the parameters
+# in a help page for a command in the console.
+helpManParameters=Parameters
+
+# LOCALIZATION NOTE (helpManNone): Some text shown under the parameters
+# heading in a help page for a command which has no parameters.
+helpManNone=None
+
+# LOCALIZATION NOTE (introHeader): The heading displayed at the top of the
+# output for the help command
+introHeader=Welcome to Firefox Developer Tools
+
+# LOCALIZATION NOTE (introBody): The text displayed at the top of the output
+# for the help command, just before the list of commands. This text is wrapped
+# inside a link to a localized MDN article
+introBody=For more information see MDN.
+
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
@@ -39,21 +39,19 @@ consoleclearDesc=Clear the console
 # LOCALIZATION NOTE (inspectDesc) A very short description of the 'inspect'
 # command. See inspectManual for a fuller description of what it does. This
 # string is designed to be shown in a menu alongside the command name, which
 # is why it should be as short as possible.
 inspectDesc=Inspect a node
 
 # LOCALIZATION NOTE (inspectManual) A fuller description of the 'inspect'
 # command, displayed when the user asks for help on what it does.
-inspectManual=Investigate the dimensions and properties of an element using \
-a CSS selector to open the DOM highlighter
+inspectManual=Investigate the dimensions and properties of an element using a CSS selector to open the DOM highlighter
 
 # LOCALIZATION NOTE (inspectNodeDesc) A very short string to describe the
 # 'node' parameter to the 'inspect' command, which is displayed in a dialog
 # when the user is using this command.
 inspectNodeDesc=CSS selector
 
 # LOCALIZATION NOTE (inspectNodeManual) A fuller description of the 'node'
 # parameter to the 'inspect' command, displayed when the user asks for help
 # on what it does.
-inspectNodeManual=A CSS selector for use with Document.querySelector which \
-identifies a single element
+inspectNodeManual=A CSS selector for use with Document.querySelector which identifies a single element
--- a/browser/themes/gnomestripe/devtools/gcli.css
+++ b/browser/themes/gnomestripe/devtools/gcli.css
@@ -41,17 +41,16 @@
 
 .gcliterm-input-node,
 .gcliterm-complete-node {
   border: none;
   -moz-appearance: none;
   height: 100%;
   vertical-align: middle;
   background-color: transparent;
-  font: 12px Consolas, "Lucida Console", monospace;
 }
 
 .gcliterm-input-node {
   padding-top: 2px;
   padding-bottom: 0;
   -moz-padding-start: 16px;
   -moz-padding-end: 0;
 }
@@ -59,34 +58,16 @@
 .gcliterm-complete-node {
   color: #FFF;
   padding-top: 4px;
   padding-bottom: 2px;
   -moz-padding-start: 21px;
   -moz-padding-end: 4px;
 }
 
-.gcli-in-valid {
-  border-bottom: none;
-}
-
-.gcli-in-incomplete {
-  color: #DDD;
-  border-bottom: 1px dotted #999;
-}
-
-.gcli-in-error {
-  color: #DDD;
-  border-bottom: 1px dotted #F00;
-}
-
-.gcli-in-ontab {
-  color: #999;
-}
-
 .gcliterm-stack-node {
   background: url("chrome://global/skin/icons/commandline.png") 4px center no-repeat;
   width: 100%;
 }
 
 .gcliterm-argfetcher {
   display: -moz-box;
   -moz-box-flex: 1;
@@ -104,29 +85,57 @@
 
 .gcliterm-hint-parent {
   width: 300px;
   padding: 10px 10px 0;
   border-top: 1px solid threedshadow;
   border-bottom: 1px solid threedshadow;
 }
 
-.gcli-help-right {
-  text-align: right;
-}
-
 .gcliterm-menu {
   display: -moz-box;
   -moz-box-flex: 1;
+  border-bottom-color: white;
+}
+
+.gcliterm-hint-scroll {
+  overflow-y: scroll;
+  border-bottom-color: threedshadow;
 }
 
 .gcliterm-hint-nospace {
   display: none;
 }
 
+.gcliterm-msg-body {
+  margin-top: 0;
+  margin-bottom: 3px;
+  -moz-margin-start: 3px;
+  -moz-margin-end: 6px;
+}
+
+/* Extract from display.css, we only want these 2 rules */
+
+.gcli-out-shortcut {
+  border: 1px solid #999;
+  border-radius: 3px;
+  padding: 0 4px;
+  margin: 0 4px;
+  font-size: 70%;
+  color: #666;
+  cursor: pointer;
+  vertical-align: bottom;
+}
+
+.gcli-out-shortcut:before {
+  color: #66F;
+  content: '\bb';
+  padding: 0 2px;
+}
+
 /*
  * The language of a console is not en_US or any other common language
  * (i.e we don't attempt to translate 'console.log(x)')
  * So we fix .gcliterm-input-node/.gcliterm-complete-node elements to be ltr.
  * As a result we also want the hints to pop up on the left (above the prompt)
  */
 .gcliterm-input-node,
 .gcliterm-complete-node,
@@ -146,16 +155,25 @@
  */
 .gcliterm-hint-parent:-moz-locale-dir(rtl),
 .hud-output-node:-moz-locale-dir(rtl) {
   direction: rtl;
 }
 
 /* From: $GCLI/mozilla/gcli/ui/gcliterm-gnomestripe.css */
 
+.gcliterm-input-node,
+.gcliterm-complete-node {
+  font: 12px "DejaVu Sans Mono", monospace;
+}
+
+.gcli-out-shortcut {
+  font-family: "DejaVu Sans Mono", monospace;
+}
+
 /* From: $GCLI/lib/gcli/ui/arg_fetch.css */
 
 .gcli-argfetch {
   width: 100%;
   box-sizing: border-box;
   -moz-box-sizing: border-box;
 }
 
@@ -176,32 +194,36 @@
 .gcli-af-paramname {
   text-align: right;
   font-size: 90%;
 }
 
 .gcli-af-required {
   font-size: 90%;
   color: #f66;
-  padding-left: 5px;
+  -moz-padding-start: 5px;
 }
 
 .gcli-af-error {
   font-size: 80%;
   color: #900;
 }
 
 .gcli-af-submit {
   text-align: right;
 }
 
 .gcli-field {
   width: 100%;
 }
 
+.gcli-field-javascript {
+  margin-bottom: 0;
+}
+
 /* From: $GCLI/lib/gcli/ui/menu.css */
 
 .gcli-menu {
   width: 100%;
   overflow: hidden;
 }
 
 .gcli-menu-field {
@@ -279,8 +301,50 @@
 .gcli-in-closebrace {
   color: #999;
 }
 
 .gcli-prompt {
   color: #66F;
   font-weight: bold;
 }
+
+/* From: $GCLI/lib/gcli/commands/help.css */
+
+.gcli-help-name {
+  text-align: end;
+}
+
+.gcli-help-arrow {
+  font-size: 70%;
+  color: #AAA;
+}
+
+.gcli-help-synopsis {
+  font-family: monospace;
+  font-weight: normal;
+  padding: 0 3px;
+  margin: 0 10px;
+  border: 1px solid #999;
+  border-radius: 3px;
+  color: #666;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.gcli-help-synopsis:before {
+  color: #66F;
+  content: '\bb';
+}
+
+.gcli-help-description {
+  margin: 0 20px;
+  padding: 0;
+}
+
+.gcli-help-parameter {
+  margin: 0 30px;
+  padding: 0;
+}
+
+.gcli-help-header {
+  margin: 10px 0 6px;
+}
--- a/browser/themes/pinstripe/devtools/gcli.css
+++ b/browser/themes/pinstripe/devtools/gcli.css
@@ -41,17 +41,16 @@
 
 .gcliterm-input-node,
 .gcliterm-complete-node {
   border: none;
   -moz-appearance: none;
   height: 100%;
   vertical-align: middle;
   background-color: transparent;
-  font: 12px Consolas, "Lucida Console", monospace;
 }
 
 .gcliterm-input-node {
   padding-top: 2px;
   padding-bottom: 0;
   -moz-padding-start: 16px;
   -moz-padding-end: 0;
 }
@@ -59,34 +58,16 @@
 .gcliterm-complete-node {
   color: #FFF;
   padding-top: 4px;
   padding-bottom: 2px;
   -moz-padding-start: 21px;
   -moz-padding-end: 4px;
 }
 
-.gcli-in-valid {
-  border-bottom: none;
-}
-
-.gcli-in-incomplete {
-  color: #DDD;
-  border-bottom: 1px dotted #999;
-}
-
-.gcli-in-error {
-  color: #DDD;
-  border-bottom: 1px dotted #F00;
-}
-
-.gcli-in-ontab {
-  color: #999;
-}
-
 .gcliterm-stack-node {
   background: url("chrome://global/skin/icons/commandline.png") 4px center no-repeat;
   width: 100%;
 }
 
 .gcliterm-argfetcher {
   display: -moz-box;
   -moz-box-flex: 1;
@@ -104,29 +85,57 @@
 
 .gcliterm-hint-parent {
   width: 300px;
   padding: 10px 10px 0;
   border-top: 1px solid threedshadow;
   border-bottom: 1px solid threedshadow;
 }
 
-.gcli-help-right {
-  text-align: right;
-}
-
 .gcliterm-menu {
   display: -moz-box;
   -moz-box-flex: 1;
+  border-bottom-color: white;
+}
+
+.gcliterm-hint-scroll {
+  overflow-y: scroll;
+  border-bottom-color: threedshadow;
 }
 
 .gcliterm-hint-nospace {
   display: none;
 }
 
+.gcliterm-msg-body {
+  margin-top: 0;
+  margin-bottom: 3px;
+  -moz-margin-start: 3px;
+  -moz-margin-end: 6px;
+}
+
+/* Extract from display.css, we only want these 2 rules */
+
+.gcli-out-shortcut {
+  border: 1px solid #999;
+  border-radius: 3px;
+  padding: 0 4px;
+  margin: 0 4px;
+  font-size: 70%;
+  color: #666;
+  cursor: pointer;
+  vertical-align: bottom;
+}
+
+.gcli-out-shortcut:before {
+  color: #66F;
+  content: '\bb';
+  padding: 0 2px;
+}
+
 /*
  * The language of a console is not en_US or any other common language
  * (i.e we don't attempt to translate 'console.log(x)')
  * So we fix .gcliterm-input-node/.gcliterm-complete-node elements to be ltr.
  * As a result we also want the hints to pop up on the left (above the prompt)
  */
 .gcliterm-input-node,
 .gcliterm-complete-node,
@@ -146,20 +155,29 @@
  */
 .gcliterm-hint-parent:-moz-locale-dir(rtl),
 .hud-output-node:-moz-locale-dir(rtl) {
   direction: rtl;
 }
 
 /* From: $GCLI/mozilla/gcli/ui/gcliterm-pinstripe.css */
 
+.gcliterm-input-node,
+.gcliterm-complete-node {
+  font: 11px Menlo, Monaco, monospace;
+}
+
 .gcliterm-complete-node {
   padding-top: 6px !important;
 }
 
+.gcli-out-shortcut {
+  font-family: Menlo, Monaco, monospace;
+}
+
 /* From: $GCLI/lib/gcli/ui/arg_fetch.css */
 
 .gcli-argfetch {
   width: 100%;
   box-sizing: border-box;
   -moz-box-sizing: border-box;
 }
 
@@ -180,32 +198,36 @@
 .gcli-af-paramname {
   text-align: right;
   font-size: 90%;
 }
 
 .gcli-af-required {
   font-size: 90%;
   color: #f66;
-  padding-left: 5px;
+  -moz-padding-start: 5px;
 }
 
 .gcli-af-error {
   font-size: 80%;
   color: #900;
 }
 
 .gcli-af-submit {
   text-align: right;
 }
 
 .gcli-field {
   width: 100%;
 }
 
+.gcli-field-javascript {
+  margin-bottom: 0;
+}
+
 /* From: $GCLI/lib/gcli/ui/menu.css */
 
 .gcli-menu {
   width: 100%;
   overflow: hidden;
 }
 
 .gcli-menu-field {
@@ -283,8 +305,50 @@
 .gcli-in-closebrace {
   color: #999;
 }
 
 .gcli-prompt {
   color: #66F;
   font-weight: bold;
 }
+
+/* From: $GCLI/lib/gcli/commands/help.css */
+
+.gcli-help-name {
+  text-align: end;
+}
+
+.gcli-help-arrow {
+  font-size: 70%;
+  color: #AAA;
+}
+
+.gcli-help-synopsis {
+  font-family: monospace;
+  font-weight: normal;
+  padding: 0 3px;
+  margin: 0 10px;
+  border: 1px solid #999;
+  border-radius: 3px;
+  color: #666;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.gcli-help-synopsis:before {
+  color: #66F;
+  content: '\bb';
+}
+
+.gcli-help-description {
+  margin: 0 20px;
+  padding: 0;
+}
+
+.gcli-help-parameter {
+  margin: 0 30px;
+  padding: 0;
+}
+
+.gcli-help-header {
+  margin: 10px 0 6px;
+}
--- a/browser/themes/winstripe/devtools/gcli.css
+++ b/browser/themes/winstripe/devtools/gcli.css
@@ -41,17 +41,16 @@
 
 .gcliterm-input-node,
 .gcliterm-complete-node {
   border: none;
   -moz-appearance: none;
   height: 100%;
   vertical-align: middle;
   background-color: transparent;
-  font: 12px Consolas, "Lucida Console", monospace;
 }
 
 .gcliterm-input-node {
   padding-top: 2px;
   padding-bottom: 0;
   -moz-padding-start: 16px;
   -moz-padding-end: 0;
 }
@@ -59,34 +58,16 @@
 .gcliterm-complete-node {
   color: #FFF;
   padding-top: 4px;
   padding-bottom: 2px;
   -moz-padding-start: 21px;
   -moz-padding-end: 4px;
 }
 
-.gcli-in-valid {
-  border-bottom: none;
-}
-
-.gcli-in-incomplete {
-  color: #DDD;
-  border-bottom: 1px dotted #999;
-}
-
-.gcli-in-error {
-  color: #DDD;
-  border-bottom: 1px dotted #F00;
-}
-
-.gcli-in-ontab {
-  color: #999;
-}
-
 .gcliterm-stack-node {
   background: url("chrome://global/skin/icons/commandline.png") 4px center no-repeat;
   width: 100%;
 }
 
 .gcliterm-argfetcher {
   display: -moz-box;
   -moz-box-flex: 1;
@@ -104,29 +85,57 @@
 
 .gcliterm-hint-parent {
   width: 300px;
   padding: 10px 10px 0;
   border-top: 1px solid threedshadow;
   border-bottom: 1px solid threedshadow;
 }
 
-.gcli-help-right {
-  text-align: right;
-}
-
 .gcliterm-menu {
   display: -moz-box;
   -moz-box-flex: 1;
+  border-bottom-color: white;
+}
+
+.gcliterm-hint-scroll {
+  overflow-y: scroll;
+  border-bottom-color: threedshadow;
 }
 
 .gcliterm-hint-nospace {
   display: none;
 }
 
+.gcliterm-msg-body {
+  margin-top: 0;
+  margin-bottom: 3px;
+  -moz-margin-start: 3px;
+  -moz-margin-end: 6px;
+}
+
+/* Extract from display.css, we only want these 2 rules */
+
+.gcli-out-shortcut {
+  border: 1px solid #999;
+  border-radius: 3px;
+  padding: 0 4px;
+  margin: 0 4px;
+  font-size: 70%;
+  color: #666;
+  cursor: pointer;
+  vertical-align: bottom;
+}
+
+.gcli-out-shortcut:before {
+  color: #66F;
+  content: '\bb';
+  padding: 0 2px;
+}
+
 /*
  * The language of a console is not en_US or any other common language
  * (i.e we don't attempt to translate 'console.log(x)')
  * So we fix .gcliterm-input-node/.gcliterm-complete-node elements to be ltr.
  * As a result we also want the hints to pop up on the left (above the prompt)
  */
 .gcliterm-input-node,
 .gcliterm-complete-node,
@@ -146,16 +155,25 @@
  */
 .gcliterm-hint-parent:-moz-locale-dir(rtl),
 .hud-output-node:-moz-locale-dir(rtl) {
   direction: rtl;
 }
 
 /* From: $GCLI/mozilla/gcli/ui/gcliterm-winstripe.css */
 
+.gcliterm-input-node,
+.gcliterm-complete-node {
+  font: 12px Consolas, "Lucida Console", monospace;
+}
+
+.gcli-out-shortcut {
+  font-family: Consolas, Inconsolata, "Courier New", monospace;
+}
+
 /* From: $GCLI/lib/gcli/ui/arg_fetch.css */
 
 .gcli-argfetch {
   width: 100%;
   box-sizing: border-box;
   -moz-box-sizing: border-box;
 }
 
@@ -176,32 +194,36 @@
 .gcli-af-paramname {
   text-align: right;
   font-size: 90%;
 }
 
 .gcli-af-required {
   font-size: 90%;
   color: #f66;
-  padding-left: 5px;
+  -moz-padding-start: 5px;
 }
 
 .gcli-af-error {
   font-size: 80%;
   color: #900;
 }
 
 .gcli-af-submit {
   text-align: right;
 }
 
 .gcli-field {
   width: 100%;
 }
 
+.gcli-field-javascript {
+  margin-bottom: 0;
+}
+
 /* From: $GCLI/lib/gcli/ui/menu.css */
 
 .gcli-menu {
   width: 100%;
   overflow: hidden;
 }
 
 .gcli-menu-field {
@@ -279,8 +301,50 @@
 .gcli-in-closebrace {
   color: #999;
 }
 
 .gcli-prompt {
   color: #66F;
   font-weight: bold;
 }
+
+/* From: $GCLI/lib/gcli/commands/help.css */
+
+.gcli-help-name {
+  text-align: end;
+}
+
+.gcli-help-arrow {
+  font-size: 70%;
+  color: #AAA;
+}
+
+.gcli-help-synopsis {
+  font-family: monospace;
+  font-weight: normal;
+  padding: 0 3px;
+  margin: 0 10px;
+  border: 1px solid #999;
+  border-radius: 3px;
+  color: #666;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.gcli-help-synopsis:before {
+  color: #66F;
+  content: '\bb';
+}
+
+.gcli-help-description {
+  margin: 0 20px;
+  padding: 0;
+}
+
+.gcli-help-parameter {
+  margin: 0 30px;
+  padding: 0;
+}
+
+.gcli-help-header {
+  margin: 10px 0 6px;
+}
--- a/toolkit/components/prompts/src/nsPrompter.js
+++ b/toolkit/components/prompts/src/nsPrompter.js
@@ -350,32 +350,27 @@ let PromptUtils = {
         // values, lest the prompt forget to set them.
         for (let propName in obj)
             obj[propName] = propBag.getProperty(propName);
     },
 
     getTabModalPrompt : function (domWin) {
         var promptBox = null;
 
-        // Given a content DOM window, returns the chrome window it's in.
-        function getChromeWindow(aWindow) {
-            var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                                   .getInterface(Ci.nsIWebNavigation)
-                                   .QueryInterface(Ci.nsIDocShell)
-                                   .chromeEventHandler.ownerDocument.defaultView;
-            return chromeWin;
-        }
-
         try {
             // Get the topmost window, in case we're in a frame.
             var promptWin = domWin.top;
 
             // Get the chrome window for the content window we're using.
             // (Unwrap because we need a non-IDL property below.)
-            var chromeWin = getChromeWindow(promptWin).wrappedJSObject;
+            var chromeWin = promptWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                                     .getInterface(Ci.nsIWebNavigation)
+                                     .QueryInterface(Ci.nsIDocShell)
+                                     .chromeEventHandler.ownerDocument
+                                     .defaultView.wrappedJSObject;
 
             if (chromeWin.getTabModalPromptBox)
                 promptBox = chromeWin.getTabModalPromptBox(promptWin);
         } catch (e) {
             // If any errors happen, just assume no tabmodal prompter.
         }
 
         return promptBox;