Copy from experimental.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ed67a05
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+browser/build.js*
+browser/index.html
+browser/third-party
+go/bin
+go/pkg
+node_modules
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a0b7d79
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,73 @@
+export PATH:=$(VANADIUM_ROOT)/environment/cout/node/bin:$(PWD)/node_modules/.bin:$(PATH)
+export GOPATH=$(PWD)/go
+export VDLPATH=$(GOPATH)
+
+# All JS files except build.js and third party
+JS_FILES = $(shell find browser -name "*.js" -a -not -name "build.js" -a -not -path "*third-party*")
+# All HTML/CSS files except index.html and third party
+HTML_FILES = $(shell find browser -name "*.css" -a -not -path "*third-party*" -o  -name "*.html" -a -not -name "index.html" -a -not -path "*third-party*")
+
+# Builds everything
+all: node_modules browser/third-party browser/third-party/veyron browser/build.js browser/index.html $(VANADIUM_ROOT)/release/go/bin
+
+v-binaries:
+# TODO(nlacasse): Only build the binaries we need.
+	v23 go install v.io/...
+
+# Build vdl.go
+go/src/p2b/vdl/p2b.vdl.go: v-binaries
+	vdl generate -lang=go p2b/vdl
+
+# Compile p2b cli binary
+go/bin/p2b: go/src/p2b/main.go go/src/p2b/vdl/p2b.vdl.go
+	v23 go install p2b/...
+
+# Install what we need from NPM, tools such as jspm, serve, etc...
+node_modules: package.json
+	npm prune
+	npm install
+	touch node_modules
+
+# Install JSPM and Bower packages as listed in browser/package.json from JSPM and browser/bower.json from bower
+browser/third-party: browser/package.json browser/bower.json node_modules
+	cd browser && \
+	jspm install -y
+# Link a local copy of veyron.js.
+# TODO(nlacasse): Remove this and put veyron.js in package.json once we can get
+# it from npm
+	cd $(VANADIUM_ROOT)/release/javascript/core && \
+	jspm link -y npm:veyronjs@0.0.1
+	cd browser && \
+	jspm install -y -link npm:veyronjs && \
+	bower prune && \
+	bower install
+	touch browser/third-party
+
+# Bundle whole app and third-party JavaScript into a single build.js
+browser/build.js: $(JS_FILES) browser/third-party node_modules
+	cd browser; \
+	jspm setmode local; \
+	jspm bundle app build.js
+
+# Bundle all app web components and third-party web components into a single index.html
+browser/index.html: $(HTML_FILES) browser/build.js node_modules
+	cd browser; \
+	vulcanize -o index.html app.html
+
+# Serve
+start: browser/index.html
+	serve browser/. --port 8000
+
+# Continuously watch for changes to .js, .html or .css files.
+# Rebundle the appropriate file (build.js and/or index.html) when local files change
+watch:
+	watch -n 1 make
+
+# Clean all build artifacts
+clean:
+	rm -rf browser/third-party
+	rm -rf node_modules
+	rm -f browser/index.html
+	rm -f browser/build.js
+
+.PHONY: start clean watch v-binaries
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..839ba4b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+# Pipe to Browser
+P2B allows one to pipe anything from shell console to the browser. Data being piped to the browser then is displayed in a graphical and formatted way by a "viewer" Viewers are pluggable pieces of code that know how to handle and display a stream of data.
+
+For example one can do:
+
+``
+echo "Hi!" | p2b google/p2b/jane/console
+``
+
+or
+
+``
+cat cat.jpg | p2b -binary google/p2b/jane/image
+``
+
+where **google/p2b/jane** is the Object name where p2b service is running in the browser. The suffix **console** or **image** specifies what viewer should be used to display the data.
+
+Please see the help page inside the P2B application for detailed tutorials.
+
+## Building and Running
+To build
+``
+make
+``
+To run
+``
+make start #Starts a web server at 8080
+``
+and then navigate to http://localhost:8080
+
+To stop simply Ctrl-C the console that started it
+
+To clean
+``
+make clean
+``
diff --git a/browser/.bowerrc b/browser/.bowerrc
new file mode 100644
index 0000000..eb14785
--- /dev/null
+++ b/browser/.bowerrc
@@ -0,0 +1,3 @@
+{
+  "directory" : "third-party"
+}
diff --git a/browser/actions/add-pipe-viewer.js b/browser/actions/add-pipe-viewer.js
new file mode 100644
index 0000000..a284731
--- /dev/null
+++ b/browser/actions/add-pipe-viewer.js
@@ -0,0 +1,84 @@
+/*
+ * AddPipeViewer action can be used to add a new viewer to the pipes view
+ * this action can be run at anytime and user can be on any view and this action
+ * will still work.
+ * Depending on user preferences, user might be presented with a confirmation
+ * dialog to accept seeing the incoming pipe.
+ * @fileoverview
+ */
+
+import { Logger } from 'libs/logs/logger'
+import { register, trigger } from 'libs/mvc/actions'
+
+import { get as getPipeViewer } from 'pipe-viewers/manager'
+
+import { displayError } from 'actions/display-error'
+import { navigatePipesPage } from 'actions/navigate-pipes-page'
+import { redirectPipe } from 'actions/redirect-pipe'
+
+import { LoadingView } from 'views/loading/view'
+
+import { pipesViewInstance } from 'runtime/context'
+
+var log = new Logger('actions/add-pipe-viewer');
+var ACTION_NAME = 'addPipeViewer';
+var pipesPerNameCounter = {};
+
+/*
+ * Registers the add pipe viewer action
+ */
+export function registerAddPipeViewerAction() {
+  register(ACTION_NAME, actionHandler);
+}
+
+/*
+ * Triggers the add pipe viewer action
+ */
+export function addPipeViewer(name, stream) {
+  return trigger(ACTION_NAME, name, stream);
+}
+
+/*
+ * Handles the addPipeViewer action.
+ * @param {string} name Name of the Pipe Viewer that is requested to play the stream.
+ * @param {Veyron.Stream} stream Stream of bytes from the p2b client.
+ *
+ * @private
+ */
+function actionHandler(name, stream) {
+  log.debug('addPipeViewer action triggered');
+
+  // Book keeping of number of pipe-viewers per name, we use this to generate
+  // display names and keys like image #3
+  var count = (pipesPerNameCounter[name] || 0) + 1;
+  pipesPerNameCounter[name] = count;
+  var tabKey = name + count;
+  var tabName = 'Loading...';
+
+  // Get the plugin that can render the stream, ask it to play it and display
+  // the element returned by the pipeViewer.
+  getPipeViewer(name).then((pipeViewer) => {
+    tabName = pipeViewer.name + ' #' + count
+    return pipeViewer.play(stream);
+  }).then((pipeViewerView) => {
+    // replace the loading view with the actual viewerView
+    pipesViewInstance.replaceTabView(tabKey, tabName, pipeViewerView);
+  }).catch((e) => { displayError(e); });
+
+  // Add a new tab and show a loading indicator for now,
+  // then replace the loading view with the actual viewer when ready
+  // close the stream when tab closes
+  var loadingView = new LoadingView();
+  pipesViewInstance.addTab(tabKey, tabName, loadingView, () => {
+    stream.end();
+  });
+
+  // Add the redirect stream action
+  var icon = 'hardware:cast';
+  pipesViewInstance.addToolbarAction(tabKey, icon, () => {
+    redirectPipe(stream, name);
+  });
+
+  // Take the user to the pipes view.
+  navigatePipesPage();
+}
diff --git a/browser/actions/display-error.js b/browser/actions/display-error.js
new file mode 100644
index 0000000..6baf042
--- /dev/null
+++ b/browser/actions/display-error.js
@@ -0,0 +1,43 @@
+/*
+ * Error action displays the error page displaying the given error
+ * @fileoverview
+ */
+
+import { Logger } from 'libs/logs/logger'
+import { register, trigger } from 'libs/mvc/actions'
+
+import { ErrorView } from 'views/error/view'
+
+import { page } from 'runtime/context'
+
+var log = new Logger('actions/display-error');
+var ACTION_NAME = 'error';
+
+/*
+ * Registers the error action
+ */
+export function registerDisplayErrorAction() {
+  register(ACTION_NAME, actionHandler);
+}
+
+/*
+ * Triggers the error action
+ */
+export function displayError(err) {
+  return trigger(ACTION_NAME, err);
+}
+
+/*
+ * Handles the error action.
+ *
+ * @private
+ */
+function actionHandler(err) {
+  log.debug('error action triggered');
+
+  // Create an error view
+  var errorView = new ErrorView(err);
+
+  // Display the error view in Home sub-page area
+  page.setSubPageView('home', errorView);
+}
diff --git a/browser/actions/navigate-help.js b/browser/actions/navigate-help.js
new file mode 100644
index 0000000..9456a02
--- /dev/null
+++ b/browser/actions/navigate-help.js
@@ -0,0 +1,43 @@
+/*
+ * Navigates to help page
+ * @fileoverview
+ */
+import { Logger } from 'libs/logs/logger'
+import { register, trigger } from 'libs/mvc/actions'
+
+import { state as publishState } from 'services/pipe-to-browser-server'
+
+import { page } from 'runtime/context'
+
+import { HelpView } from 'views/help/view'
+
+var log = new Logger('actions/navigate-help');
+var ACTION_NAME = 'help';
+
+/*
+ * Registers the action
+ */
+export function registerHelpAction() {
+  register(ACTION_NAME, actionHandler);
+}
+
+/*
+ * Triggers the action
+ */
+export function navigateHelp() {
+  return trigger(ACTION_NAME);
+}
+
+/*
+ * Handles the action.
+ *
+ * @private
+ */
+function actionHandler() {
+  log.debug('navigate help triggered');
+
+  // create a help view
+  var helpView = new HelpView(publishState);
+
+  page.setSubPageView('help', helpView);
+}
diff --git a/browser/actions/navigate-home-page.js b/browser/actions/navigate-home-page.js
new file mode 100644
index 0000000..a05e38d
--- /dev/null
+++ b/browser/actions/navigate-home-page.js
@@ -0,0 +1,107 @@
+/*
+ * Home action displays the Home page. Home could be status or publish view
+ * depending on the state of the P2B service.
+ * @fileoverview
+ */
+
+import { Logger } from 'libs/logs/logger'
+import { register, trigger } from 'libs/mvc/actions'
+
+import { publish, stopPublishing, state as publishState } from 'services/pipe-to-browser-server'
+
+import { displayError } from 'actions/display-error'
+import { addPipeViewer } from 'actions/add-pipe-viewer'
+
+import { PublishView } from 'views/publish/view'
+import { StatusView } from 'views/status/view'
+
+import { page } from 'runtime/context'
+
+var log = new Logger('actions/navigate-home-page');
+var ACTION_NAME = 'home';
+
+/*
+ * Registers the home action
+ */
+export function registerNavigateHomePageAction() {
+  register(ACTION_NAME, actionHandler);
+}
+
+/*
+ * Triggers the home action
+ */
+export function navigateHomePage() {
+  return trigger(ACTION_NAME);
+}
+
+/*
+ * Handles the home action.
+ *
+ * @private
+ */
+function actionHandler() {
+  log.debug('home action triggered');
+
+  var mainView;
+
+  // Show status view if already published, otherwise show publish view
+  if (publishState.published) {
+    showStatusView();
+  } else {
+    showPublishView();
+  }
+}
+
+/*
+ * Displays the Status view
+ *
+ * @private
+ */
+function showStatusView() {
+  // Create a status view  and bind (dynamic) publish state with the view
+  var statusView = new StatusView(publishState);
+
+  // Stop when user tells us to stop the service
+  statusView.onStopAction(() => {
+    stopPublishing().then(function() {
+      navigateHomePage();
+    }).catch((e) => { displayError(e); });
+  });
+
+  // Display the status view in main content area and select the sidebar item
+  page.title = 'Status';
+  page.setSubPageView('home', statusView);
+}
+
+/*
+ * Displays the Publish view
+ *
+ * @private
+ */
+function showPublishView() {
+  // Create a publish view
+  var publishView = new PublishView();
+
+  // Publish p2b when user tells us to do so and then show status page.
+  publishView.onPublishAction((publishName) => {
+    publish(publishName, pipeRequestHandler).then(function() {
+      showStatusView();
+    }).catch((e) => { displayError(e); });
+  });
+
+  // Display the publish view in main content area and select the sidebar item
+  page.title = 'Publish';
+  page.setSubPageView('home', publishView);
+}
+
+/*
+ * pipeRequestHandler is called by the p2b service whenever a new request comes in.
+ * We simply delegate to the addPipeViewer action.
+ * @param {string} name Name of the Pipe Viewer that is requested to play the stream.
+ * @param {Veyron.Stream} stream Stream of bytes from the p2b client.
+ *
+ * @private
+ */
+function pipeRequestHandler(name, stream) {
+  return addPipeViewer(name, stream);
+}
diff --git a/browser/actions/navigate-neighborhood.js b/browser/actions/navigate-neighborhood.js
new file mode 100644
index 0000000..e8bbea8
--- /dev/null
+++ b/browser/actions/navigate-neighborhood.js
@@ -0,0 +1,49 @@
+/*
+ * Navigates to neighborhood page displaying list of P2B names that are online
+ * @fileoverview
+ */
+
+import { Logger } from 'libs/logs/logger'
+import { register, trigger } from 'libs/mvc/actions'
+
+import { displayError } from 'actions/display-error'
+import { page } from 'runtime/context'
+
+import { NeighborhoodView } from 'views/neighborhood/view'
+import { getAll as getAllPublishedP2BNames } from 'services/pipe-to-browser-namespace'
+
+var log = new Logger('actions/navigate-neighborhood');
+var ACTION_NAME = 'neighborhood';
+
+/*
+ * Registers the action
+ */
+export function registerNavigateNeigbourhoodAction() {
+  register(ACTION_NAME, actionHandler);
+}
+
+/*
+ * Triggers the action
+ */
+export function navigateNeigbourhood() {
+  return trigger(ACTION_NAME);
+}
+
+/*
+ * Handles the action.
+ *
+ * @private
+ */
+function actionHandler() {
+  log.debug('navigate neighborhood triggered');
+
+  // create an neighborhood view
+  var neighborhoodView = new NeighborhoodView();
+
+  // get all the online names and set it on the view
+  getAllPublishedP2BNames().then((allNames) => {
+    neighborhoodView.existingNames = allNames;
+  }).catch((e) => { displayError(e); });
+
+  page.setSubPageView('neighborhood', neighborhoodView);
+}
diff --git a/browser/actions/navigate-pipes-page.js b/browser/actions/navigate-pipes-page.js
new file mode 100644
index 0000000..9e870a3
--- /dev/null
+++ b/browser/actions/navigate-pipes-page.js
@@ -0,0 +1,40 @@
+/*
+ * Pipes action displays the Pipes page. It is normally triggered by clicking
+ * the Pipes navigation item in the side bar
+ * @fileoverview
+ */
+
+import { Logger } from 'libs/logs/logger'
+import { register, trigger } from 'libs/mvc/actions'
+
+import { page, pipesViewInstance } from 'runtime/context'
+
+var log = new Logger('actions/navigate-pipes-page');
+var ACTION_NAME = 'pipes';
+
+/*
+ * Registers the pipes action
+ */
+export function registerNavigatePipesPageAction() {
+  register(ACTION_NAME, actionHandler);
+}
+
+/*
+ * Triggers the pipes action
+ */
+export function navigatePipesPage(err) {
+  return trigger(ACTION_NAME, err);
+}
+
+/*
+ * Handles the pipes action.
+ *
+ * @private
+ */
+function actionHandler() {
+  log.debug('pipes action triggered');
+
+  // display the singleton pipesViewInstance main content area
+  page.title = 'Pipes';
+  page.setSubPageView('pipes', pipesViewInstance);
+}
diff --git a/browser/actions/redirect-pipe.js b/browser/actions/redirect-pipe.js
new file mode 100644
index 0000000..6bcb93b
--- /dev/null
+++ b/browser/actions/redirect-pipe.js
@@ -0,0 +1,69 @@
+/*
+ * Redirects a stream to another veyron name. It prompts the user to pick
+ * a Veyron name before redirecting and allows the user to chose between
+ * redirecting all the data or just new incoming data.
+ * @fileoverview
+ */
+import { Logger } from 'libs/logs/logger'
+import { register, trigger } from 'libs/mvc/actions'
+
+import { page } from 'runtime/context'
+
+import { RedirectPipeDialogView } from 'views/redirect-pipe-dialog/view'
+import { pipe } from 'services/pipe-to-browser-client'
+import { getAll as getAllPublishedP2BNames } from 'services/pipe-to-browser-namespace'
+
+var log = new Logger('actions/redirect-pipe');
+var ACTION_NAME = 'redirect-pipe';
+
+/*
+ * Registers the redirect pipe action
+ */
+export function registerRedirectPipeAction() {
+  register(ACTION_NAME, actionHandler);
+}
+
+/*
+ * Triggers the redirect pipe action
+ * @param {stream} stream Stream object to redirect
+ * @param {string} currentPluginName name of the current plugin
+ */
+export function redirectPipe(stream, currentPluginName) {
+  return trigger(ACTION_NAME, stream, currentPluginName);
+}
+
+/*
+ * Handles the redirect pipe action.
+ *
+ * @private
+ */
+function actionHandler(stream, currentPluginName) {
+  log.debug('redirect pipe action triggered');
+
+  // display a dialog asking user where to redirect and whether to redirect
+  // all the data or just new data.
+  var dialog = new RedirectPipeDialogView();
+  dialog.open();
+
+  // if user decides to redirect, copy the stream and pipe it.
+  dialog.onRedirectAction((name, newDataOnly) => {
+    var copyStream = stream.copier.copy(newDataOnly);
+
+    pipe(name, copyStream).then(() => {
+      page.showToast('Redirected successfully to ' + name);
+    }).catch((e) => {
+      page.showToast('FAILED to redirect to ' + name + '. Please see console for error details.');
+      log.debug('FAILED to redirect to', name, e);
+    });
+  });
+
+  // also get the list of all existing P2B names in the namespace and supply it to the dialog
+  getAllPublishedP2BNames().then((allNames) => {
+    // append current plugin name to the veyron names for better UX
+    dialog.existingNames = allNames.map((n) => {
+      return n + '/' + currentPluginName;
+    });
+  }).catch((e) => {
+    log.debug('getAllPublishedP2BNames failed', e);
+  });
+}
diff --git a/browser/app.html b/browser/app.html
new file mode 100644
index 0000000..f3cac9b
--- /dev/null
+++ b/browser/app.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
+  <meta name="mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="description" content="Pipe To Browser (p2b) is a utility built with veyron technology that allows piping of stdout and std from console into local or remote browser windows. Different plugins exist to format the incoming data and display them in an appropriate and interactive format.">
+  <title>Pipe To Browser - because life is too short to stare at unformatted stdout text, and is hard enough already not to have a spell-checker for stdin</title>
+
+  <script src="third-party/traceur-runtime.js"></script>
+  <script src="third-party/system.js"></script>
+  <script src="config.js"></script>
+  <script src="build.js"></script>
+
+  <link rel="import" href="views/page/component.html"/>
+
+  <style type="text/css">
+    body {
+      font-family: 'Roboto', sans-serif;
+      color: rgba(0,0,0,0.87);
+    }
+  </style>
+</head>
+<body>
+
+
+  <script>
+    window.addEventListener('polymer-ready', function(e) {
+      System.import('app').then(function(app) {
+        app.start();
+      }).catch(function(e) {
+        console.error(e);
+      });
+    });
+  </script>
+</body>
+</html>
diff --git a/browser/app.js b/browser/app.js
new file mode 100644
index 0000000..e2b1f2d
--- /dev/null
+++ b/browser/app.js
@@ -0,0 +1,84 @@
+import { Logger } from 'libs/logs/logger'
+
+import { registerNavigateHomePageAction,  navigateHomePage } from 'actions/navigate-home-page'
+import { registerDisplayErrorAction } from 'actions/display-error'
+import { registerAddPipeViewerAction } from 'actions/add-pipe-viewer'
+import { registerNavigatePipesPageAction, navigatePipesPage } from 'actions/navigate-pipes-page'
+import { registerNavigateNeigbourhoodAction, navigateNeigbourhood } from 'actions/navigate-neighborhood'
+import { registerHelpAction, navigateHelp } from 'actions/navigate-help'
+import { registerRedirectPipeAction } from 'actions/redirect-pipe'
+
+import { SubPageItem } from 'views/page/view'
+
+import { page } from 'runtime/context'
+
+var log = new Logger('app');
+
+export function start() {
+  log.debug('start called');
+
+  // Initialize a new page, sets up toolbar and action bar for the app
+  initPageView();
+
+  // Register the action handlers for the application
+  registerActions();
+
+  // Start by triggering the home action
+  navigateHomePage();
+}
+
+/*
+ * Registers the action handlers for the application.
+ * Actions are cohesive pieces of functionality that can be triggered from
+ * any other action in a decoupled way by just using the action name.
+ *
+ * @private
+ */
+function registerActions() {
+
+  log.debug('registering actions');
+
+  registerNavigateHomePageAction();
+  registerDisplayErrorAction();
+  registerAddPipeViewerAction();
+  registerNavigatePipesPageAction();
+  registerNavigateNeigbourhoodAction();
+  registerRedirectPipeAction();
+  registerHelpAction();
+}
+
+/*
+ * Constructs a new page, sets up toolbar and action bar for the app
+ *
+ * @private
+ */
+function initPageView() {
+
+  // Home, Pipes and Help are top level sub-pages
+  var homeSubPageItem = new SubPageItem('home');
+  homeSubPageItem.name = 'Home';
+  homeSubPageItem.icon = 'home';
+  homeSubPageItem.onActivate = navigateHomePage;
+  page.subPages.push(homeSubPageItem);
+
+  var pipesSubPageItem = new SubPageItem('pipes');
+  pipesSubPageItem.name = 'Pipes';
+  pipesSubPageItem.icon = 'arrow-forward';
+  pipesSubPageItem.onActivate = navigatePipesPage;
+  page.subPages.push(pipesSubPageItem);
+
+  var neighborhoodSubPageItem = new SubPageItem('neighborhood');
+  neighborhoodSubPageItem.name = 'Neighborhood';
+  neighborhoodSubPageItem.icon = 'social:circles-extended';
+  neighborhoodSubPageItem.onActivate = navigateNeigbourhood;
+  page.subPages.push(neighborhoodSubPageItem);
+
+  var helpSubPageItem = new SubPageItem('help');
+  helpSubPageItem.name = 'Help';
+  helpSubPageItem.icon = 'help';
+  helpSubPageItem.onActivate = navigateHelp;
+
+  page.subPages.push(helpSubPageItem);
+
+  document.body.appendChild(page.element);
+}
diff --git a/browser/bower.json b/browser/bower.json
new file mode 100644
index 0000000..177d881
--- /dev/null
+++ b/browser/bower.json
@@ -0,0 +1,15 @@
+{
+  "name": "pipe-to-browser",
+  "version": "0.0.1",
+  "dependencies": {
+    "polymer": "Polymer/polymer#~0.5.4",
+    "core-elements": "Polymer/core-elements#~0.5.4",
+    "paper-elements": "Polymer/paper-elements#~0.5.4"
+  },
+  "resolutions": {
+    "polymer": "^0.5",
+    "core-meta": "^0.5",
+    "core-transition": "^0.5",
+    "core-component-page": "^0.5"
+  }
+}
diff --git a/browser/config.js b/browser/config.js
new file mode 100644
index 0000000..0bc8f5e
--- /dev/null
+++ b/browser/config.js
@@ -0,0 +1,441 @@
+System.config({
+  "paths": {
+    "*": "*.js",
+    "pipe-viewer": "pipe-viewers/pipe-viewer.js",
+    "pipe-viewer-delegation": "pipe-viewers/pipe-viewer-delegation.js",
+    "view": "libs/mvc/view.js",
+    "logger": "libs/logs/logger.js",
+    "stream-helpers": "libs/utils/stream-helpers.js",
+    "web-component-loader": "libs/utils/web-component-loader.js",
+    "formatting": "libs/utils/formatting.js",
+    "app/*": "lib/*.js",
+    "github:*": "third-party/github/*.js",
+    "npm:*": "third-party/npm/*.js"
+  }
+});
+
+System.config({
+  "map": {
+    "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+    "npm:event-stream": "npm:event-stream@3.2.2",
+    "npm:humanize": "npm:humanize@0.0.9",
+    "stream": "github:jspm/nodelibs-stream@0.1.0",
+    "veyronjs": "npm:veyronjs@0.0.1",
+    "github:jspm/nodelibs-assert@0.1.0": {
+      "assert": "npm:assert@1.3.0"
+    },
+    "github:jspm/nodelibs-buffer@0.1.0": {
+      "buffer": "npm:buffer@3.0.1"
+    },
+    "github:jspm/nodelibs-console@0.1.0": {
+      "console-browserify": "npm:console-browserify@1.1.0"
+    },
+    "github:jspm/nodelibs-constants@0.1.0": {
+      "constants-browserify": "npm:constants-browserify@0.0.1"
+    },
+    "github:jspm/nodelibs-crypto@0.1.0": {
+      "crypto-browserify": "npm:crypto-browserify@3.9.12"
+    },
+    "github:jspm/nodelibs-events@0.1.0": {
+      "events-browserify": "npm:events-browserify@0.0.1"
+    },
+    "github:jspm/nodelibs-http@1.7.0": {
+      "Base64": "npm:Base64@0.2.1",
+      "events": "github:jspm/nodelibs-events@0.1.0",
+      "inherits": "npm:inherits@2.0.1",
+      "stream": "github:jspm/nodelibs-stream@0.1.0",
+      "url": "github:jspm/nodelibs-url@0.1.0",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "github:jspm/nodelibs-https@0.1.0": {
+      "https-browserify": "npm:https-browserify@0.0.0"
+    },
+    "github:jspm/nodelibs-os@0.1.0": {
+      "os-browserify": "npm:os-browserify@0.1.2"
+    },
+    "github:jspm/nodelibs-path@0.1.0": {
+      "path-browserify": "npm:path-browserify@0.0.0"
+    },
+    "github:jspm/nodelibs-process@0.1.1": {
+      "process": "npm:process@0.10.0"
+    },
+    "github:jspm/nodelibs-punycode@0.1.0": {
+      "punycode": "npm:punycode@1.3.2"
+    },
+    "github:jspm/nodelibs-querystring@0.1.0": {
+      "querystring": "npm:querystring@0.2.0"
+    },
+    "github:jspm/nodelibs-stream@0.1.0": {
+      "stream-browserify": "npm:stream-browserify@1.0.0"
+    },
+    "github:jspm/nodelibs-string_decoder@0.1.0": {
+      "string_decoder": "npm:string_decoder@0.10.31"
+    },
+    "github:jspm/nodelibs-timers@0.1.0": {
+      "timers-browserify": "npm:timers-browserify@1.3.0"
+    },
+    "github:jspm/nodelibs-tty@0.1.0": {
+      "tty-browserify": "npm:tty-browserify@0.0.0"
+    },
+    "github:jspm/nodelibs-url@0.1.0": {
+      "url": "npm:url@0.10.2"
+    },
+    "github:jspm/nodelibs-util@0.1.0": {
+      "util": "npm:util@0.10.3"
+    },
+    "github:jspm/nodelibs-vm@0.1.0": {
+      "vm-browserify": "npm:vm-browserify@0.0.4"
+    },
+    "github:jspm/nodelibs-zlib@0.1.0": {
+      "browserify-zlib": "npm:browserify-zlib@0.1.4"
+    },
+    "npm:asn1.js-rfc3280@1.0.0": {
+      "asn1.js": "npm:asn1.js@1.0.3"
+    },
+    "npm:asn1.js@1.0.3": {
+      "assert": "github:jspm/nodelibs-assert@0.1.0",
+      "bn.js": "npm:bn.js@1.3.0",
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "inherits": "npm:inherits@2.0.1",
+      "minimalistic-assert": "npm:minimalistic-assert@1.0.0",
+      "vm": "github:jspm/nodelibs-vm@0.1.0"
+    },
+    "npm:assert@1.3.0": {
+      "util": "npm:util@0.10.3"
+    },
+    "npm:bluebird@2.9.8": {
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:browserify-aes@1.0.0": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "create-hash": "npm:create-hash@1.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "fs": "github:jspm/nodelibs-fs@0.1.1",
+      "inherits": "npm:inherits@2.0.1",
+      "stream": "github:jspm/nodelibs-stream@0.1.0",
+      "systemjs-json": "github:systemjs/plugin-json@0.1.0"
+    },
+    "npm:browserify-rsa@1.1.1": {
+      "bn.js": "npm:bn.js@1.3.0",
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "constants": "github:jspm/nodelibs-constants@0.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0"
+    },
+    "npm:browserify-sign@2.8.0": {
+      "bn.js": "npm:bn.js@1.3.0",
+      "browserify-rsa": "npm:browserify-rsa@1.1.1",
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "elliptic": "npm:elliptic@1.0.1",
+      "inherits": "npm:inherits@2.0.1",
+      "parse-asn1": "npm:parse-asn1@2.0.0",
+      "stream": "github:jspm/nodelibs-stream@0.1.0"
+    },
+    "npm:browserify-zlib@0.1.4": {
+      "assert": "github:jspm/nodelibs-assert@0.1.0",
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "pako": "npm:pako@0.2.5",
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "readable-stream": "npm:readable-stream@1.1.13",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:buffer@3.0.1": {
+      "base64-js": "npm:base64-js@0.0.8",
+      "ieee754": "npm:ieee754@1.1.4",
+      "is-array": "npm:is-array@1.0.1"
+    },
+    "npm:commander@2.1.0": {
+      "child_process": "github:jspm/nodelibs-child_process@0.1.0",
+      "events": "github:jspm/nodelibs-events@0.1.0",
+      "fs": "github:jspm/nodelibs-fs@0.1.1",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:console-browserify@1.1.0": {
+      "assert": "github:jspm/nodelibs-assert@0.1.0",
+      "date-now": "npm:date-now@0.1.4",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:constants-browserify@0.0.1": {
+      "systemjs-json": "github:systemjs/plugin-json@0.1.0"
+    },
+    "npm:core-util-is@1.0.1": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0"
+    },
+    "npm:create-ecdh@1.0.3": {
+      "bn.js": "npm:bn.js@1.3.0",
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "elliptic": "npm:elliptic@1.0.1"
+    },
+    "npm:create-hash@1.1.0": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "fs": "github:jspm/nodelibs-fs@0.1.1",
+      "inherits": "npm:inherits@2.0.1",
+      "ripemd160": "npm:ripemd160@1.0.0",
+      "sha.js": "npm:sha.js@2.3.6",
+      "stream": "github:jspm/nodelibs-stream@0.1.0"
+    },
+    "npm:create-hmac@1.1.3": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "create-hash": "npm:create-hash@1.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "inherits": "npm:inherits@2.0.1",
+      "stream": "github:jspm/nodelibs-stream@0.1.0"
+    },
+    "npm:crypto-browserify@3.9.12": {
+      "browserify-aes": "npm:browserify-aes@1.0.0",
+      "browserify-sign": "npm:browserify-sign@2.8.0",
+      "create-ecdh": "npm:create-ecdh@1.0.3",
+      "create-hash": "npm:create-hash@1.1.0",
+      "create-hmac": "npm:create-hmac@1.1.3",
+      "diffie-hellman": "npm:diffie-hellman@3.0.1",
+      "inherits": "npm:inherits@2.0.1",
+      "pbkdf2-compat": "npm:pbkdf2-compat@3.0.1",
+      "public-encrypt": "npm:public-encrypt@1.1.2",
+      "randombytes": "npm:randombytes@2.0.1"
+    },
+    "npm:diffie-hellman@3.0.1": {
+      "bn.js": "npm:bn.js@1.3.0",
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "miller-rabin": "npm:miller-rabin@1.1.5",
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "randombytes": "npm:randombytes@2.0.1",
+      "systemjs-json": "github:systemjs/plugin-json@0.1.0"
+    },
+    "npm:duplexer@0.1.1": {
+      "stream": "github:jspm/nodelibs-stream@0.1.0"
+    },
+    "npm:elliptic@1.0.1": {
+      "bn.js": "npm:bn.js@1.3.0",
+      "brorand": "npm:brorand@1.0.5",
+      "hash.js": "npm:hash.js@1.0.2",
+      "inherits": "npm:inherits@2.0.1",
+      "systemjs-json": "github:systemjs/plugin-json@0.1.0"
+    },
+    "npm:es6-shim@0.20.4": {
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:event-stream@3.2.2": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "duplexer": "npm:duplexer@0.1.1",
+      "from": "npm:from@0.1.3",
+      "map-stream": "npm:map-stream@0.1.0",
+      "pause-stream": "npm:pause-stream@0.0.11",
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "split": "npm:split@0.3.3",
+      "stream": "github:jspm/nodelibs-stream@0.1.0",
+      "stream-combiner": "npm:stream-combiner@0.0.4",
+      "through": "npm:through@2.3.6",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:events-browserify@0.0.1": {
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:from@0.1.3": {
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "stream": "github:jspm/nodelibs-stream@0.1.0"
+    },
+    "npm:hash.js@1.0.2": {
+      "inherits": "npm:inherits@2.0.1"
+    },
+    "npm:https-browserify@0.0.0": {
+      "http": "github:jspm/nodelibs-http@1.7.0"
+    },
+    "npm:humanize@0.0.9": {
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:inherits@2.0.1": {
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:map-stream@0.1.0": {
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "stream": "github:jspm/nodelibs-stream@0.1.0",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:miller-rabin@1.1.5": {
+      "bn.js": "npm:bn.js@1.3.0",
+      "brorand": "npm:brorand@1.0.5"
+    },
+    "npm:minimatch@1.0.0": {
+      "lru-cache": "npm:lru-cache@2.5.0",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "sigmund": "npm:sigmund@1.0.0"
+    },
+    "npm:nan@1.0.0": {
+      "path": "github:jspm/nodelibs-path@0.1.0"
+    },
+    "npm:options@0.0.6": {
+      "fs": "github:jspm/nodelibs-fs@0.1.1"
+    },
+    "npm:os-browserify@0.1.2": {
+      "os": "github:jspm/nodelibs-os@0.1.0"
+    },
+    "npm:pako@0.2.5": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "fs": "github:jspm/nodelibs-fs@0.1.1",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "util": "github:jspm/nodelibs-util@0.1.0",
+      "zlib": "github:jspm/nodelibs-zlib@0.1.0"
+    },
+    "npm:parse-asn1@2.0.0": {
+      "asn1.js": "npm:asn1.js@1.0.3",
+      "asn1.js-rfc3280": "npm:asn1.js-rfc3280@1.0.0",
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "pemstrip": "npm:pemstrip@0.0.1",
+      "systemjs-json": "github:systemjs/plugin-json@0.1.0"
+    },
+    "npm:path-browserify@0.0.0": {
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:pause-stream@0.0.11": {
+      "through": "npm:through@2.3.6"
+    },
+    "npm:pbkdf2-compat@3.0.1": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "child_process": "github:jspm/nodelibs-child_process@0.1.0",
+      "create-hmac": "npm:create-hmac@1.1.3",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "systemjs-json": "github:systemjs/plugin-json@0.1.0"
+    },
+    "npm:public-encrypt@1.1.2": {
+      "bn.js": "npm:bn.js@1.3.0",
+      "browserify-rsa": "npm:browserify-rsa@1.1.1",
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "parse-asn1": "npm:parse-asn1@2.0.0"
+    },
+    "npm:punycode@1.3.2": {
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:randombytes@2.0.1": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:readable-stream@1.1.13": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "core-util-is": "npm:core-util-is@1.0.1",
+      "events": "github:jspm/nodelibs-events@0.1.0",
+      "inherits": "npm:inherits@2.0.1",
+      "isarray": "npm:isarray@0.0.1",
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "stream": "npm:stream-browserify@1.0.0",
+      "string_decoder": "npm:string_decoder@0.10.31",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:ripemd160@1.0.0": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:sha.js@2.3.6": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "fs": "github:jspm/nodelibs-fs@0.1.1",
+      "inherits": "npm:inherits@2.0.1",
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:sigmund@1.0.0": {
+      "http": "github:jspm/nodelibs-http@1.7.0",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:split@0.3.3": {
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "string_decoder": "github:jspm/nodelibs-string_decoder@0.1.0",
+      "through": "npm:through@2.3.6",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:stream-browserify@1.0.0": {
+      "events": "github:jspm/nodelibs-events@0.1.0",
+      "inherits": "npm:inherits@2.0.1",
+      "readable-stream": "npm:readable-stream@1.1.13"
+    },
+    "npm:stream-combiner@0.0.4": {
+      "duplexer": "npm:duplexer@0.1.1"
+    },
+    "npm:string_decoder@0.10.31": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0"
+    },
+    "npm:through@2.3.6": {
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "stream": "github:jspm/nodelibs-stream@0.1.0"
+    },
+    "npm:timers-browserify@1.3.0": {
+      "process": "npm:process@0.10.0"
+    },
+    "npm:url@0.10.2": {
+      "assert": "github:jspm/nodelibs-assert@0.1.0",
+      "punycode": "npm:punycode@1.3.2",
+      "querystring": "github:jspm/nodelibs-querystring@0.1.0",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    },
+    "npm:util@0.10.3": {
+      "inherits": "npm:inherits@2.0.1",
+      "process": "github:jspm/nodelibs-process@0.1.1"
+    },
+    "npm:veyronjs@0.0.1": {
+      "assert": "github:jspm/nodelibs-assert@0.1.0",
+      "bluebird": "npm:bluebird@2.9.8",
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "child_process": "github:jspm/nodelibs-child_process@0.1.0",
+      "console": "github:jspm/nodelibs-console@0.1.0",
+      "constants": "github:jspm/nodelibs-constants@0.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "dgram": "github:jspm/nodelibs-dgram@0.1.0",
+      "dns": "github:jspm/nodelibs-dns@0.1.0",
+      "es6-shim": "npm:es6-shim@0.20.4",
+      "eventemitter2": "npm:eventemitter2@0.4.14",
+      "events": "github:jspm/nodelibs-events@0.1.0",
+      "fs": "github:jspm/nodelibs-fs@0.1.1",
+      "http": "github:jspm/nodelibs-http@1.7.0",
+      "https": "github:jspm/nodelibs-https@0.1.0",
+      "is-browser": "npm:is-browser@2.0.1",
+      "lru-cache": "npm:lru-cache@2.5.0",
+      "minimatch": "npm:minimatch@1.0.0",
+      "module": "github:jspm/nodelibs-module@0.1.0",
+      "net": "github:jspm/nodelibs-net@0.1.0",
+      "os": "github:jspm/nodelibs-os@0.1.0",
+      "path": "github:jspm/nodelibs-path@0.1.0",
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "punycode": "github:jspm/nodelibs-punycode@0.1.0",
+      "querystring": "github:jspm/nodelibs-querystring@0.1.0",
+      "stream": "github:jspm/nodelibs-stream@0.1.0",
+      "string_decoder": "github:jspm/nodelibs-string_decoder@0.1.0",
+      "systemjs-json": "github:systemjs/plugin-json@0.1.0",
+      "timers": "github:jspm/nodelibs-timers@0.1.0",
+      "tls": "github:jspm/nodelibs-tls@0.1.0",
+      "tty": "github:jspm/nodelibs-tty@0.1.0",
+      "url": "github:jspm/nodelibs-url@0.1.0",
+      "util": "github:jspm/nodelibs-util@0.1.0",
+      "vm": "github:jspm/nodelibs-vm@0.1.0",
+      "ws": "npm:ws@0.4.32",
+      "xtend": "npm:xtend@4.0.0",
+      "zlib": "github:jspm/nodelibs-zlib@0.1.0"
+    },
+    "npm:vm-browserify@0.0.4": {
+      "indexof": "npm:indexof@0.0.1"
+    },
+    "npm:ws@0.4.32": {
+      "buffer": "github:jspm/nodelibs-buffer@0.1.0",
+      "commander": "npm:commander@2.1.0",
+      "crypto": "github:jspm/nodelibs-crypto@0.1.0",
+      "events": "github:jspm/nodelibs-events@0.1.0",
+      "http": "github:jspm/nodelibs-http@1.7.0",
+      "https": "github:jspm/nodelibs-https@0.1.0",
+      "nan": "npm:nan@1.0.0",
+      "options": "npm:options@0.0.6",
+      "process": "github:jspm/nodelibs-process@0.1.1",
+      "stream": "github:jspm/nodelibs-stream@0.1.0",
+      "tinycolor": "npm:tinycolor@0.0.1",
+      "tls": "github:jspm/nodelibs-tls@0.1.0",
+      "url": "github:jspm/nodelibs-url@0.1.0",
+      "util": "github:jspm/nodelibs-util@0.1.0"
+    }
+  }
+});
+
diff --git a/browser/libs/css/common-style.css b/browser/libs/css/common-style.css
new file mode 100644
index 0000000..cc263ea
--- /dev/null
+++ b/browser/libs/css/common-style.css
@@ -0,0 +1,32 @@
+.hidden {
+  display: none !important;
+}
+
+.invisible {
+  visibility: hidden;
+}
+
+.secondary-text {
+  color: rgba(0,0,0,.54);
+}
+
+.no-wrap {
+  white-space: nowrap;
+}
+
+.screen-reader {
+  position: absolute;
+  top: -1000px;
+  left: -1000px;
+  width: 0px !important;
+  height: 0px !important;
+}
+
+@-webkit-keyframes blink {
+  0% {
+    opacity: 1;
+  }
+  100% {
+    opacity: 0.05;
+  }
+}
diff --git a/browser/libs/logs/logger.js b/browser/libs/logs/logger.js
new file mode 100644
index 0000000..2e1b343
--- /dev/null
+++ b/browser/libs/logs/logger.js
@@ -0,0 +1,15 @@
+/*
+ * Logger represents a module that can write logging messages to the console.
+ * @param {string} prefix A string that will be prefixed to every log message
+ * @class
+ */
+export class Logger {
+  constructor(prefix) {
+    prefix = prefix || 'Error';
+    this.prefix_ = prefix;
+  }
+
+  debug(...args) {
+    console.log('DEBUG: ' + this.prefix_ + ':', ...args);
+  }
+};
diff --git a/browser/libs/mvc/actions.js b/browser/libs/mvc/actions.js
new file mode 100644
index 0000000..0e6c465
--- /dev/null
+++ b/browser/libs/mvc/actions.js
@@ -0,0 +1,39 @@
+/*
+ * Actions are a similar concept to routes and simply provide an indirection
+ * through string names to register and call functions.
+ *
+ * Using actions to group and call larger, self-contained functionality like
+ * page transitions allows the framework to provide undo/back support by using
+ * the history API or localStorage.
+ *
+ * Action handlers that are registered for an action, normally are controllers
+ * that glue application services and state with views and handle events.
+ * @overview
+ */
+
+var registeredHandlers = {};
+
+/*
+ * Registers an action and makes it available to be called using only a name.
+ * @param {string} name Action's identifier
+ * @param {function} handler Callback handler to register for the action.
+ * handler will be called with the arguments pass from the caller when action
+ * is triggered.
+ */
+export function register(name, handler) {
+  registeredHandlers[name] = handler;
+}
+
+/*
+ * Calls an action's registered handler.
+ * @param {string} name Action's identifier
+ * @param {*} [...] args Arguments to be passed to the registered handler.
+ * @return {*} Any result returned from the registered handler
+ */
+export function trigger(name, ...args) {
+  var handler = registeredHandlers[name];
+  if(!handler) {
+    throw new Error('No handler registered for action: ' + name);
+  }
+  return handler(...args);
+}
diff --git a/browser/libs/mvc/view.js b/browser/libs/mvc/view.js
new file mode 100644
index 0000000..e3b98dc
--- /dev/null
+++ b/browser/libs/mvc/view.js
@@ -0,0 +1,19 @@
+/*
+ * Base class for all views.
+ * View is a simple wrapper that represents a DOM element and exposes the public
+ * API of that element. Web Components should be used to encapsulate View
+ * functionality under a single DOM element and handle event binding/triggering,
+ * templating, life-cycle management and attribute exposure and therefore those
+ * features are not duplicated here.
+ * @param {DOMelement} el DOM element this view wraps.
+ * @class
+ */
+export class View {
+	constructor(el) {
+		this._el = el;
+	}
+
+	get element() {
+		return this._el;
+	}
+}
diff --git a/browser/libs/ui-components/blackhole/blackhole.jpg b/browser/libs/ui-components/blackhole/blackhole.jpg
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/browser/libs/ui-components/blackhole/blackhole.jpg
diff --git a/browser/libs/ui-components/blackhole/component.css b/browser/libs/ui-components/blackhole/component.css
new file mode 100644
index 0000000..e6be710
--- /dev/null
+++ b/browser/libs/ui-components/blackhole/component.css
@@ -0,0 +1,38 @@
+:host {
+  display: block;
+  position: relative;
+  margin: 0;
+  padding: 0;
+  background-image: url('blackhole.jpg');
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: 50% 50%;
+  height: 100%;
+  width: 100%;
+}
+
+.spinner {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  margin-left: -14px;
+  margin-top: -14px;
+}
+
+.attribution {
+  position: absolute;
+  top: 0;
+  right: 0;
+  background-color: rgb(0, 0, 0);
+  padding: 0.75em;
+  font-size: 0.7em;
+  text-decoration: none;
+  font-style: normal;
+  opacity: 0.3;
+  color: #fafafa;
+}
+
+.attribution a {
+  color: #02a8f3;
+  text-decoration: none;
+}
diff --git a/browser/libs/ui-components/blackhole/component.html b/browser/libs/ui-components/blackhole/component.html
new file mode 100644
index 0000000..5e51981
--- /dev/null
+++ b/browser/libs/ui-components/blackhole/component.html
@@ -0,0 +1,29 @@
+<link rel="import" href="../../../third-party/polymer/polymer.html">
+<!--
+p2b-ui-components-blackhole is a simple image of a blackhole in the middle of a galaxy
+when blackhole is running, a spinner gif is displayed in the middle of the blackhole.
+-->
+<polymer-element name="p2b-blackhole">
+  <template>
+    <link rel="stylesheet" href="../../css/common-style.css">
+    <link rel="stylesheet" href="component.css">
+    <img class="spinner {{ !running ? 'hidden' : '' }}" src="{{ running ? 'libs/ui-components/common/spinner.gif' : '' }}" alt="Represents a blackhole destroying objects falling into it."/>
+    <cite class="attribution">Photo from Wikipedia <a target="_blank" href="http://en.wikipedia.org/wiki/Black_hole#mediaviewer/File:BH_LMC.png">by Alan R</a> (CC BY-SA 2.5)</cite>
+  </template>
+  <script>
+    Polymer('p2b-blackhole', {
+      /*
+       * Sets the blackhole in motion.
+       */
+      start: function() {
+        this.running = true;
+      },
+      /*
+       * Brings the blackhole to a halt!
+       */
+      stop: function() {
+        this.running = false;
+      }
+    });
+    </script>
+</polymer-element>
diff --git a/browser/libs/ui-components/common/spinner.gif b/browser/libs/ui-components/common/spinner.gif
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/browser/libs/ui-components/common/spinner.gif
diff --git a/browser/libs/ui-components/data-grid/filter/common.css b/browser/libs/ui-components/data-grid/filter/common.css
new file mode 100644
index 0000000..1d9bd16
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/filter/common.css
@@ -0,0 +1,14 @@
+.filter-container {
+  margin-bottom: 1.5em;
+}
+
+.filter-container h3 {
+  margin: 0;
+  margin-bottom: -10px;
+}
+
+.filter-container h3 {
+  font-size: 1em;
+  color: #9e9e9e;
+  font-weight: normal;
+}
diff --git a/browser/libs/ui-components/data-grid/filter/select/component.css b/browser/libs/ui-components/data-grid/filter/select/component.css
new file mode 100644
index 0000000..899893f
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/filter/select/component.css
@@ -0,0 +1,19 @@
+paper-checkbox, paper-radio-button {
+  padding: 16px 16px 0px 12px !important;
+}
+
+paper-checkbox::shadow #ink[checked] {
+  color: #4285f4;
+}
+
+paper-checkbox::shadow #checkbox.checked {
+  border-color: #4285f4;
+}
+
+paper-radio-button::shadow #ink[checked] {
+  color: #e91e63;
+}
+
+paper-radio-button::shadow #onRadio {
+  background-color: #e91e63;
+}
diff --git a/browser/libs/ui-components/data-grid/filter/select/component.html b/browser/libs/ui-components/data-grid/filter/select/component.html
new file mode 100644
index 0000000..72249d4
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/filter/select/component.html
@@ -0,0 +1,69 @@
+<link rel="import" href="../../../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../../../third-party/paper-checkbox/paper-checkbox.html">
+<link rel="import" href="../../../../../third-party/paper-radio-button/paper-radio-button.html">
+<link rel="import" href="../../../../../third-party/paper-radio-group/paper-radio-group.html">
+<polymer-element name="p2b-grid-filter-select" attributes="multiple key label" grid-filter expects-grid-state>
+  <template>
+    <link rel="stylesheet" href="component.css">
+    <link rel="stylesheet" href="../common.css">
+    <content id="content" select="*"></content>
+    <div class="filter-container">
+      <h3>{{label}}</h3>
+      <template if="{{ multiple }}" bind>
+        <core-selector id="multiSelector" multi selectedAttribute="checked" valueattr="value" core-select="{{updateGridState}}" selected="{{ selected }}">
+          <template repeat="{{ item in items }}">
+            <paper-checkbox value="{{ item.value }}" on-change="{{ updateGridState }}" label="{{ item.label }}"></paper-checkbox>
+          </template>
+        </core-selector>
+      </template>
+
+      <template if="{{ !multiple }}" bind>
+        <paper-radio-group id="singleSelector" valueattr="value" selected="{{ selected }}">
+          <template repeat="{{ item in items }}">
+            <paper-radio-button value="{{ item.value }}" on-change="{{ updateGridState }}" label="{{ item.label }}"></paper-radio-button>
+          </template>
+        </paper-radio-group>
+      </template>
+    </div>
+  </template>
+  <script>
+    Polymer('p2b-grid-filter-select', {
+     /*
+      * Whether multiple items can be selected
+      * @type {boolean}
+      */
+      multiple: false,
+
+      /*
+       * Key that will be added to filters map passed to the fetch() function of your data source.
+       * @type {string}
+       */
+      key: '',
+
+      ready: function() {
+        // find the selected items from the child nodes
+        this.items = Array.prototype.slice.call(this.$.content.getDistributedNodes());
+        for(var i=0; i < this.items.length; i++){
+          if(this.items[i].checked) {
+            if(this.multiple) {
+              this.selected = this.selected || [];
+              this.selected.push(this.items[i].value);
+            } else {
+              this.selected = this.items[i].value;
+            }
+          }
+        }
+      },
+
+      updateGridState: function() {
+        if( this.multiple ) {
+          // quirk: we need to copy the array so change is observed. .slice() does that
+          this.gridState.filters[this.key] = this.$.multiSelector.selected.slice();
+        } else {
+          this.gridState.filters[this.key] = this.$.singleSelector.selected.slice();
+        }
+      }
+
+    });
+    </script>
+</polymer-element>
diff --git a/browser/libs/ui-components/data-grid/filter/select/item/component.css b/browser/libs/ui-components/data-grid/filter/select/item/component.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/filter/select/item/component.css
diff --git a/browser/libs/ui-components/data-grid/filter/select/item/component.html b/browser/libs/ui-components/data-grid/filter/select/item/component.html
new file mode 100644
index 0000000..8ea8d5e
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/filter/select/item/component.html
@@ -0,0 +1,25 @@
+<link rel="import" href="../../../../../../third-party/polymer/polymer.html">
+<polymer-element name="p2b-grid-filter-select-item" attributes="label checked value">
+  <script>
+    Polymer('p2b-grid-filter-select-item', {
+      /*
+       * Label text for the item
+       * @type {string}
+       */
+      label: '',
+
+      /*
+       * Whether toggle is checked or not
+       * @type {boolean}
+       */
+      checked: false,
+
+      /*
+       * Value that will available as filters[key] in the fetch() function of your data source.
+       * Where key is the key of the select filter that contains this item
+       * @type {string}
+       */
+      value: ''
+    });
+    </script>
+</polymer-element>
diff --git a/browser/libs/ui-components/data-grid/filter/toggle/component.css b/browser/libs/ui-components/data-grid/filter/toggle/component.css
new file mode 100644
index 0000000..eb2aa4b
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/filter/toggle/component.css
@@ -0,0 +1,3 @@
+paper-toggle-button {
+  padding: 16px 16px 0px 12px !important;
+}
diff --git a/browser/libs/ui-components/data-grid/filter/toggle/component.html b/browser/libs/ui-components/data-grid/filter/toggle/component.html
new file mode 100644
index 0000000..800dc01
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/filter/toggle/component.html
@@ -0,0 +1,36 @@
+<link rel="import" href="../../../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../../../third-party/paper-toggle-button/paper-toggle-button.html">
+<polymer-element name="p2b-grid-filter-toggle" attributes="key checked label" grid-filter expects-grid-state>
+  <template>
+    <link rel="stylesheet" href="component.css">
+    <link rel="stylesheet" href="../common.css">
+    <div class="filter-container">
+      <h3>{{label}}</h3>
+      <paper-toggle-button id="toggle" on-change="{{ updateGridState }}" checked?="{{ checked }}"></paper-toggle-button>
+    </div>
+  </template>
+  <script>
+    Polymer('p2b-grid-filter-toggle', {
+      /*
+       * Label text for the toggle filter
+       * @type {string}
+       */
+      label: '',
+
+      /*
+       * Whether toggle is checked or not
+       * @type {boolean}
+       */
+      checked: false,
+
+      /*
+       * Key that will be added to filters map passed to the fetch() function of your data source.
+       * @type {string}
+       */
+      key: '',
+      updateGridState: function() {
+        this.gridState.filters[this.key] = this.$.toggle.checked;
+      }
+    });
+    </script>
+</polymer-element>
diff --git a/browser/libs/ui-components/data-grid/grid/cell/renderer.css b/browser/libs/ui-components/data-grid/grid/cell/renderer.css
new file mode 100644
index 0000000..033db1c
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/grid/cell/renderer.css
@@ -0,0 +1,9 @@
+.cell {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  padding: 0.75em 0.5em;
+}
+
+:host {
+  overflow: hidden;
+}
diff --git a/browser/libs/ui-components/data-grid/grid/cell/renderer.html b/browser/libs/ui-components/data-grid/grid/cell/renderer.html
new file mode 100644
index 0000000..514bebe
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/grid/cell/renderer.html
@@ -0,0 +1,17 @@
+<link rel="import" href="../../../../../third-party/polymer/polymer.html">
+<polymer-element name="p2b-grid-cell-renderer" extends="td" attributes="data">
+  <template>
+    <link rel="stylesheet" href="renderer.css">
+    <link rel="stylesheet" href="../../../../css/common-style.css">
+    <div class="cell {{ {'secondary-text': !data.primary, 'no-wrap': !data.wrap} | tokenList }}">
+      <content></content>
+    </div>
+  </template>
+  <script>
+   /*
+    * @private
+    */
+    Polymer('p2b-grid-cell-renderer', {
+    });
+  </script>
+</polymer-element>
diff --git a/browser/libs/ui-components/data-grid/grid/column/component.html b/browser/libs/ui-components/data-grid/grid/column/component.html
new file mode 100644
index 0000000..cbb8052
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/grid/column/component.html
@@ -0,0 +1,64 @@
+<link rel="import" href="../../../../../third-party/polymer/polymer.html">
+<polymer-element name="p2b-grid-column" grid-column attributes="label sortable key primary wrap flex minFlex priority">
+  <script>
+    Polymer('p2b-grid-column', {
+      /*
+       * Label text for the column
+       * @type {string}
+       */
+      label: '',
+
+      /*
+       * number specifying how flexible the width of the column is
+       * compared to other columns. For instance for a grid with three columns
+       * flex values of 1, 3, 1. means the middle column needs to be three times
+       * as wide as the other two.
+       * Flex value of 0 means the column should not be displayed.
+       * Flex values for all the columns can add up to any value.
+       * @type {integer}
+       */
+      flex: 1,
+
+      /*
+       * minimum Flex value that this column can be reduced to by the responsive data grid
+       * Defaults to 1 meaning column can be reduced to 1 flex as available space shrinks.
+       * @type {integer}
+       */
+      minFlex: 1,
+
+      /*
+       * specifies the importance of this column. Responsive grid uses this number
+       * to decide which columns to reduce/hide when available space shrinks.
+       * Lower number means more important.
+       * @type {integer}
+       */
+      priority: 1,
+
+      /*
+       * whether this column is sortable
+       * @type {boolean}
+       */
+      sortable: false,
+
+      /*
+       * whether this the primary column of the grid.
+       * Normally there is a single column that other columns are dependents on
+       * @type {boolean}
+       */
+      primary: false,
+
+
+      /*
+       * whether text inside the cells of this columns are allowed to wrap.
+       * @type {boolean}
+       */
+      wrap: false,
+
+      /*
+       * Key that will be pass as sort.key to fetch() function of your data source.
+       * @type {string}
+       */
+      key: ''
+    });
+  </script>
+</polymer-element>
diff --git a/browser/libs/ui-components/data-grid/grid/column/renderer.css b/browser/libs/ui-components/data-grid/grid/column/renderer.css
new file mode 100644
index 0000000..d2a49ce
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/grid/column/renderer.css
@@ -0,0 +1,26 @@
+:host {
+  color: rgba(0,0,0, 0.26);
+  font-weight: normal;
+  text-align: left;
+  white-space: nowrap;
+}
+
+paper-button {
+  text-align: left;
+  width: 100%;
+  text-transform: none;
+}
+
+paper-button[disabled] {
+  background: inherit !important;
+  color: inherit !important;
+}
+
+paper-button[disabled] {
+  background: inherit !important;
+  color: inherit !important;
+}
+
+paper-button::shadow #ripple {
+  color: #0f9d58;
+}
diff --git a/browser/libs/ui-components/data-grid/grid/column/renderer.html b/browser/libs/ui-components/data-grid/grid/column/renderer.html
new file mode 100644
index 0000000..208ff49
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/grid/column/renderer.html
@@ -0,0 +1,48 @@
+<link rel="import" href="../../../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../../../third-party/paper-button/paper-button.html">
+<polymer-element name="p2b-grid-column-renderer" extends="th" attributes="data gridState" expects-grid-state>
+  <template>
+    <link rel="stylesheet" href="renderer.css">
+    <paper-button label="{{ formattedLabel }}" disabled?="{{!data.sortable}}" on-tap="{{ updateGridState }}"></paper-button>
+  </template>
+  <script>
+   /*
+    * @private
+    */
+    Polymer('p2b-grid-column-renderer', {
+      'observe': {
+        'data.flex' : 'updateWidth',
+        'data.totalFlex' : 'updateWidth'
+      },
+
+      domReady: function() {
+        this.updateWidth();
+      },
+
+      updateWidth: function() {
+        // calculate the width value based on flex and total flex of the whole grid.
+        this.style.width = (this.data.flex / this.data.totalFlex) * 100 + '%';
+      },
+
+      updateGridState:function() {
+        if( !this.data.sortable ) {
+          return;
+        }
+        this.gridState.sort.ascending = !this.gridState.sort.ascending;
+        this.gridState.sort.key = this.data.key;
+      },
+
+      get formattedLabel() {
+        if (!this.data.sortable || this.gridState.sort.key != this.data.key) {
+          return this.data.label;
+        }
+
+        if (this.gridState.sort.ascending) {
+          return this.data.label + ' \u21A5'; // up wedge unicode character
+        } else {
+          return this.data.label + ' \u21A7'; // down wedge unicode character
+        }
+      }
+    });
+  </script>
+</polymer-element>
diff --git a/browser/libs/ui-components/data-grid/grid/component.css b/browser/libs/ui-components/data-grid/grid/component.css
new file mode 100644
index 0000000..b309a31
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/grid/component.css
@@ -0,0 +1,100 @@
+table {
+  border-collapse: collapse;
+  table-layout: fixed;
+  width: 100%;
+  border-spacing: 0;
+  margin-top: 15px;
+}
+
+thead tr {
+  border-bottom: solid 1px rgba(0,0,0, .12);
+  padding-bottom: 0.75em;
+}
+
+thead tr th {
+  position: relative;
+}
+
+td {
+  overflow: hidden;
+  vertical-align: top;
+}
+
+table tbody tr:nth-child(2) td {
+  padding-top: 15px;
+}
+
+.more-icon {
+  fill: #0a7e07;
+  color: #0a7e07;
+}
+
+paper-dialog {
+  max-width: 80em;
+  width: 80vw;
+}
+
+.more-dialog-content .heading {
+  font-size: 1.0em;
+  padding-top: 0.2em;
+  padding-bottom: 0.2em;
+  color: #4285f4;
+  border-bottom: 1px solid rgba(0,0,0,0.05);
+  font-weight: normal;
+  text-transform: uppercase;
+  margin: 0;
+}
+
+.more-dialog-content .details {
+  margin-bottom: 1em;
+}
+
+.search-fab {
+  position: absolute;
+  right: 20px;
+  top: 10px;
+  background-color: #03a9f4;
+}
+
+#searchTools {
+  box-shadow: rgba(0, 0, 0, 0.14902) 2px 2px 4px;
+  background-color: #f5f5f5;
+  padding: 1em;
+  padding-bottom: 0;
+}
+
+.result-count {
+  font-size: 0.8em;
+  color: #616161;
+  float: right;
+}
+
+.info-column {
+  text-align: center;
+}
+
+[moreInfoOnly] {
+  display: none;
+}
+
+.more-dialog-content [moreInfoOnly] {
+  display: initial;
+}
+
+.more-dialog-content [gridOnly] {
+  display:none;
+}
+
+.paginator {
+  display: inline-block;
+  border: solid 1px rgba(0, 0, 0, 0.05);
+  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15);
+  color: rgba(0, 0, 0, 0.54);
+  fill: rgba(0, 0, 0, 0.54);
+  margin: 1em;
+  font-size: 0.9em;
+}
+
+.paginator paper-icon-button {
+  vertical-align: middle;
+}
diff --git a/browser/libs/ui-components/data-grid/grid/component.html b/browser/libs/ui-components/data-grid/grid/component.html
new file mode 100644
index 0000000..4f555ab
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/grid/component.html
@@ -0,0 +1,446 @@
+<link rel="import" href="../../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../../third-party/paper-icon-button/paper-icon-button.html">
+<link rel="import" href="../../../../third-party/paper-dialog/paper-dialog.html">
+<link rel="import" href="../../../../third-party/paper-dialog/paper-dialog-transition.html">
+<link rel="import" href="../../../../third-party/paper-fab/paper-fab.html">
+<link rel="import" href="../../../../third-party/core-collapse/core-collapse.html">
+<link rel="import" href="row/renderer.html">
+<link rel="import" href="cell/renderer.html">
+<link rel="import" href="column/renderer.html">
+
+<polymer-element name="p2b-grid" attributes="summary dataSource defaultSortKey defaultSortAscending pageSize">
+  <template>
+    <link rel="stylesheet" href="../../../css/common-style.css">
+    <link rel="stylesheet" href="component.css">
+    <div id="templates"></div>
+    <core-collapse id="searchTools">
+      <div class="result-count">Showing {{ dataSourceResult.length }} items of {{totalNumItems}}</div>
+      <div>
+        <content select="[grid-search]"></content>
+      </div>
+      <div>
+        <content select="[grid-filter]"></content>
+      </div>
+    </core-collapse>
+    <table id="table" summary="{{ summary }}" cellpadding="0" cellpadding="0" border="0"  style="visibility:hidden" >
+      <thead>
+        <tr>
+          <th is="p2b-grid-column-renderer" gridState="{{ gridState }}" data="{{ col.columnData }}" repeat="{{ col in columns }}" template></th>
+          <th style="width:40px">&nbsp;<span class="screen-reader">More info</span>
+            <paper-fab class="search-fab" focused icon="search" on-tap="{{ toggleSearchTools }}"></paper-fab>
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <!-- quirk: Shadow Dom breaks parent-child relationships in HTML, this causes issues with
+         elements like table. Ideally we could have had <grid><grid-row><grid-cell> but we can't do
+         that yet since the tr and td rendered by <grid-row> <grid-cell> will be in shadow Dom and isolated.
+         Chromium bug: https://code.google.com/p/chromium/issues/detail?id=374315
+         W3C Spec bug: https://www.w3.org/Bugs/Public/show_bug.cgi?id=15616
+         -->
+        <tr is="p2b-grid-row-renderer" repeat="{{ item in dataSourceResult }}" template>
+          <td is="p2b-grid-cell-renderer" data="{{ col.columnData }}" repeat="{{ col in columns }}" template>
+            <template ref="{{ col.cellTemplateId }}" bind></template>
+          </td>
+          <td class="info-column">
+            <paper-icon-button on-click="{{ showMoreInfo }}" class="more-icon" icon="more-vert" title="more info"></paper-icon-button
+            >
+          </td>
+        </tr>
+      </tbody>
+    </table>
+
+    <!-- Pagination -->
+    <template if="{{totalNumPages > 1}}">
+      <div class="paginator">
+        <paper-icon-button title="Previous page" icon="hardware:keyboard-arrow-left"
+        class="{{ {invisible : pageNumber == 1} | tokenList }}" on-click="{{ previousPage }}"></paper-icon-button>
+        <span>Page {{ pageNumber }} of {{ totalNumPages }}</span>
+        <paper-icon-button title="Next page" icon="hardware:keyboard-arrow-right"
+        class="{{ {invisible : onLastPage } | tokenList }}" on-click="{{ nextPage }}"></paper-icon-button>
+      </div>
+    </template>
+
+    <!-- Dialog that displays all columns and their values when more info icon activated -->
+    <paper-dialog id="dialog" heading="Details" transition="paper-dialog-transition-bottom">
+      <template id="moreInfoTemplate" bind>
+        <div class="more-dialog-content">
+          <template repeat="{{ item in selectedItems }}">
+            <template repeat="{{ col in columns }}">
+              <h3 class="heading">{{ col.columnData.label }}</h3>
+              <div class="details"><template ref="{{ col.cellTemplateId }}" bind></template></div>
+            </template>
+          </template>
+        </div>
+      </template>
+      <paper-button label="Close" dismissive></paper-button>
+    </paper-dialog>
+
+  </template>
+  <script>
+    /*
+     * Reusable grid that can host search, filters and supports sortable columns and custom cell renderer
+     * @example usage:
+
+           <p2b-grid defaultSortKey="firstName"
+            defaultSortAscending
+            dataSource="{{ myContactsDataSource }}"
+            summary="Displays your contacts in a tabular format">
+
+            <!-- Search contacts-->
+            <p2b-grid-search label="Search Contacts"></p2b-grid-search>
+
+            <!-- Filter for circles -->
+            <p2b-grid-filter-select multiple key="circle" label="Circles">
+              <p2b-grid-filter-select-item checked label="Close Friends" value="close"></p2b-grid-filter-select-item>
+              <p2b-grid-filter-select-item label="Colleagues" value="far"></p2b-grid-filter-select-item>
+            </p2b-grid-filter-select>
+
+            <!-- Toggle to allow filtering by online mode-->
+            <p2b-grid-filter-toggle key="online" label="Show online only" checked></p2b-grid-filter-toggle>
+
+            <!-- Columns, sorting and cell templates -->
+            <p2b-grid-column sortable label="First Name" key="firstName" />
+              <template>{{ item.firstName }}</template>
+            </p2b-grid-column>
+
+            <p2b-grid-column sortable label="Last Name" key="lastName" />
+              <template>
+                <span style="text-transform:uppercase;">
+                  {{ item.lastName }}
+                </span>
+              </template>
+            </p2b-grid-column>
+
+            <p2b-grid-column label="Circle" key="circle"/>
+              <template>
+                <img src="images\circls\{{ item.circle }}.jpg" alt="in {{ item.circle }} circle"><img>
+              </template>
+            </p2b-grid-column>
+
+          </p2b-grid>
+
+     * DataSource attribute expects an object that has a fetch(search, sort, filters) method. Please see
+     * documentation on DataSource property for details.
+     */
+    Polymer('p2b-grid', {
+      /*
+       * DataSource is an object that has a fetch(search, sort, filters) method where
+       * search{key<string>} is current search keyword
+       * sort{key<string>, ascending<bool>} current sort key and direction
+       * filter{map{key<string>, values<Array>}} Map of filter keys to currently selected filter values
+       * search, sort and filters are provided by the grid control whenever they are changed by the user.
+       * DataSource is called automatically by the grid when user interacts with the component
+       * Grid does some batching of user actions and only calls fetch when needed in a requestAnimationFrame
+       * Keys provided for sort and filters correspond to keys set in the markup when constructing the grid.
+       * DataSource.fetch() is expected to return an array of filtered sorted results of the items.
+       */
+      dataSource: null,
+
+      /*
+       * Summary for the grid.
+       * @type {string}
+       */
+      summary: '',
+
+      /*
+       * Initial sort key
+       * @type {string}
+       */
+      defaultSortKey: '',
+
+      /*
+       * Initial sort direction
+       * @type {string}
+       */
+      defaultSortAscending: false,
+
+      /*
+       * Number if items displayed in each page.
+       * Defaults to 20
+       * @type {integer}
+       */
+      pageSize: 20,
+
+      showMoreInfo: function(e) {
+        var item = e.target.templateInstance.model.item;
+        this.selectedItems = [item];
+        this.$.dialog.toggle();
+      },
+
+      ready: function() {
+
+        // private property fields
+        this.columns = [];
+        this.pageNumber = 1;
+        this.dataSource = null;
+        this.cachedDataSourceResult = [];
+        this.dataSourceResult = [];
+        this.gridState = {
+          sort: {
+            key: '',
+            ascending: false
+          },
+          search: {
+            keyword: ''
+          },
+          filters: {}
+        },
+
+        // set the default sort and direction on the state object
+        this.gridState.sort.key = this.defaultSortKey;
+        this.gridState.sort.ascending = this.defaultSortAscending;
+
+        this.initTemplates(); // loads cell templates
+        this.initGridStateDependents(); //  initialize filters and search
+        this.initGridStateObserver(); // observe changes to grid state by filters
+
+      },
+
+      /*
+       * Called by Polymer when DOM is read.
+       * @private
+       */
+      domReady: function() {
+        this.adjustFlexWidths();
+        this.$.table.style.visibility = 'visible';
+      },
+
+      /*
+       * Called by Polymer when dataSource attribute changes.
+       * @private
+       */
+      dataSourceChanged: function() {
+        this.refresh(true);
+      },
+
+      /*
+       * Sets up an object observer to get any mutations on the grid state object.
+       * Filters or sortable columns can change the state and we like to refresh
+       * when changes happens.
+       * @private
+       */
+      initGridStateObserver: function() {
+        var self = this;
+        for (key in this.gridState) {
+          var observer = new ObjectObserver(this.gridState[key])
+          observer.open(function() {
+            // refresh the grid on any mutations and go back to page one
+            self.refresh(true);
+          });
+        }
+      },
+
+      /*
+       * Copies the cell templates as defined by the user for each column into
+       * the grid so that we can reference them in a loop.
+       * quirk: Need to reference them by Id so a new Id is generated for each one
+       * Ids are scoped in the shadow DOM so no collisions.
+       * @private
+       */
+      initTemplates: function() {
+        var self = this;
+        var columnNodes = this.querySelectorAll('[grid-column]');
+        var totalFlex = 0;
+        for (var i = 0; i < columnNodes.length; i++) {
+          var col = columnNodes[i];
+          var cellTemplate = col.querySelector('template');
+          this.originalContext = cellTemplate.model;
+          var cellTemplateId = "userProvidedCellTemplate" + i;
+          cellTemplate.id = cellTemplateId;
+          this.$.templates.appendChild(cellTemplate);
+          totalFlex += col.flex;
+          col.origFlex = col.flex;
+          this.columns.push({
+            cellTemplateId: cellTemplateId,
+            columnData: col
+          });
+        }
+
+        // add up the total value of flex attribute on each column and add it to data
+        this.columns.forEach(function(c) {
+          c.columnData.totalFlex = totalFlex;
+        });
+
+        // readjust the widths on resize
+        var previousTableWidth = self.$.table.offsetWidth;
+        onResizeHandler = function() {
+          var newWidth = self.$.table.offsetWidth;
+          if (newWidth != previousTableWidth && newWidth > 0) {
+            self.adjustFlexWidths();
+          }
+          previousTableWidth = newWidth;
+        };
+
+        // quirks: since there is no good way to know if width changes, we pull.
+        // window.resize does not cover all resize cases
+        this.resizeInterval = setInterval(onResizeHandler, 50);
+      },
+
+      /*
+       * Called by Polymer when DOM is gone. We need to unbind custom event listeners here.
+       * @private
+       */
+      detached: function() {
+        clearInterval(this.resizeInterval);
+      },
+
+      /*
+       * Provide the grid state to any component that expects it so they can mutate
+       * without the grid needing to know about them at all.
+       * @private
+       */
+      initGridStateDependents: function() {
+        var gridStateDependents = this.querySelectorAll('[expects-grid-state]');
+        for (var i = 0; i < gridStateDependents.length; i++) {
+          gridStateDependents[i].gridState = this.gridState;
+        }
+      },
+
+      /*
+       * Refreshed the grid by fetching the data again and updating the UI in the next render tick
+       * @param {bool} goBackToPageOne Optional parameter indicating that grid should go back
+       * to page 1 after refresh. false by default
+       */
+      refresh: function(goBackToPageOne) {
+        var self = this;
+        requestAnimationFrame(function() {
+          if (goBackToPageOne) {
+            self.pageNumber = 1;
+          }
+          self.updateDataSource();
+        });
+      },
+
+      /*
+       * Performs responsive changes for the grid.
+       * Values of flex, minFlex and priority attributes on the grid column decides
+       * the responsive behavior.
+       * Grid assumes the original total number of columns can fit on a 768px width,
+       * if width of the grid container is less than that, then it starts to reduce
+       * flex values for each columns in reverse priority one by one until it reaches
+       * the minFlex value for all columns.
+       * If it still needs to reduce the width of the table at this point, it starts hiding
+       * columns in reverse priority order.
+       */
+      adjustFlexWidths: function() {
+        var minWidth = 768;
+        var tableWidth = this.$.table.offsetWidth;
+
+        // reset to original flex values
+        for (var i = 0; i < this.columns.length; i++) {
+          var col = this.columns[i];
+          col.columnData.flex = col.columnData.origFlex;
+        }
+
+        if (tableWidth === 0 || tableWidth >= minWidth) {
+          return;
+        }
+
+        // total of all flex values from all columns
+        var totalFlex = this.columns.reduce( function(prev, col) {
+          return prev + col.columnData.flex;
+        }, 0);
+
+        // number of pixels per flex point
+        var pixelPerFlex = Math.floor(tableWidth / totalFlex);
+        // number of flex points we need to eliminate to same pixelPerFlex as the minWidth case
+        var numFlexToEliminate = Math.ceil((minWidth - tableWidth) / pixelPerFlex);
+
+        // sort from least important to most important
+        var sortedColumnsData = this.columns.map(function(col) {
+          return col.columnData
+        }).sort(function(a, b) {
+          return b.priority - a.priority;
+        });
+
+        // first try to reduce each flex value until we hit min-flex for each column
+        var numElimintedFlex = 0
+        var numIrreducableColumns = 0;
+        var numColumns = sortedColumnsData.length;
+        while (numElimintedFlex < numFlexToEliminate && numIrreducableColumns < numColumns) {
+          for (var i = 0; i < numColumns; i++) {
+            var col = sortedColumnsData[i];
+            if (col.flex > col.minFlex) {
+              col.flex--;
+              numElimintedFlex++;
+            } else {
+              numIrreducableColumns++;
+            }
+          }
+        }
+
+        // if still need to reduce, start eliminating whole columns based on priority
+        // never eliminate the top priority column, hence only iterate to numColumns - 1
+        if (numElimintedFlex < numFlexToEliminate) {
+          for (var i = 0; i < numColumns - 1 && numElimintedFlex < numFlexToEliminate; i++) {
+            var col = sortedColumnsData[i];
+            numElimintedFlex += col.flex;
+            col.flex = 0;
+          }
+        }
+
+        // update the new totalFlex for each column
+        this.columns.forEach(function(c) {
+          c.columnData.totalFlex = totalFlex - numFlexToEliminate;
+        });
+      },
+
+      /*
+       * dataSourceResult is what the UI binds to and integrate over.
+       * Only fetches data if scheduled to do so
+       * @private
+       */
+      updateDataSource: function() {
+        if (!this.dataSource) {
+          return;
+        }
+
+        // fetch the data
+        this.cachedDataSourceResult = this.dataSource.fetch(
+          this.gridState.search,
+          this.gridState.sort,
+          this.gridState.filters
+        );
+
+        // page the data
+        this.totalNumItems = this.cachedDataSourceResult.length;
+        // if there less data than current page number, go back to page 1
+        if (this.totalNumItems < (this.pageNumber - 1) * this.pageSize) {
+          this.pageNumber = 1;
+        }
+        this.totalNumPages = Math.ceil(this.totalNumItems / this.pageSize);
+        this.onLastPage = this.totalNumPages == this.pageNumber;
+
+        // skip and take
+        var startIndex = (this.pageNumber - 1) * this.pageSize;
+        var endIndex = startIndex + this.pageSize;
+        this.cachedDataSourceResult = this.cachedDataSourceResult.slice(startIndex, endIndex);
+
+        this.dataSourceResult = this.cachedDataSourceResult;
+      },
+
+      /*
+       * collapse/show search and filter container.
+       * @private
+       */
+      toggleSearchTools: function() {
+        this.$.searchTools.toggle();
+      },
+
+      nextPage: function() {
+        if (!this.onLastPage) {
+          this.pageNumber++;
+          this.refresh();
+        }
+      },
+
+      previousPage: function() {
+        if (this.pageNumber > 1) {
+          this.pageNumber--;
+          this.refresh();
+        }
+      }
+    });
+  </script>
+</polymer-element>
diff --git a/browser/libs/ui-components/data-grid/grid/row/renderer.css b/browser/libs/ui-components/data-grid/grid/row/renderer.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/grid/row/renderer.css
diff --git a/browser/libs/ui-components/data-grid/grid/row/renderer.html b/browser/libs/ui-components/data-grid/grid/row/renderer.html
new file mode 100644
index 0000000..02c8c02
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/grid/row/renderer.html
@@ -0,0 +1,14 @@
+<link rel="import" href="../../../../../third-party/polymer/polymer.html">
+<polymer-element name="p2b-grid-row-renderer" extends="tr">
+  <template>
+    <link rel="stylesheet" href="renderer.css">
+    <content></content>
+  </template>
+  <script>
+   /*
+    * @private
+    */
+    Polymer('p2b-grid-row-renderer', {
+    });
+  </script>
+</polymer-element>
diff --git a/browser/libs/ui-components/data-grid/search/component.css b/browser/libs/ui-components/data-grid/search/component.css
new file mode 100644
index 0000000..539d9f1
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/search/component.css
@@ -0,0 +1,4 @@
+paper-input {
+  width: 50%;
+  color: #9e9e9e;
+}
diff --git a/browser/libs/ui-components/data-grid/search/component.html b/browser/libs/ui-components/data-grid/search/component.html
new file mode 100644
index 0000000..93b51db
--- /dev/null
+++ b/browser/libs/ui-components/data-grid/search/component.html
@@ -0,0 +1,23 @@
+<link rel="import" href="../../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../../third-party/paper-input/paper-input.html">
+<polymer-element name="p2b-grid-search" grid-search expects-grid-state attributes="label">
+  <template>
+    <link rel="stylesheet" href="component.css">
+    <paper-input inputValue="{{ value }}" id="search" label="{{ label }}"></paper-input>
+  </template>
+  <script>
+    /*
+     * Renders a search box inside the grid components
+     */
+    Polymer('p2b-grid-search', {
+      /*
+       * Label text for the search
+       * @type {string}
+       */
+      label: '',
+      valueChanged: function() {
+        this.gridState.search.keyword = this.value;
+      }
+    });
+  </script>
+</polymer-element>
diff --git a/browser/libs/utils/byte-object-stream-adapter.js b/browser/libs/utils/byte-object-stream-adapter.js
new file mode 100644
index 0000000..bb051c2
--- /dev/null
+++ b/browser/libs/utils/byte-object-stream-adapter.js
@@ -0,0 +1,24 @@
+import { default as Stream } from "stream"
+import { default as buffer } from "buffer"
+
+var Transform = Stream.Transform;
+var Buffer = buffer.Buffer;
+
+/*
+ * Adapts a stream of byte arrays in object mode to a regular stream of Buffer
+ * @class
+ */
+export class ByteObjectStreamAdapter extends Transform {
+  constructor() {
+    super();
+    this._writableState.objectMode = true;
+    this._readableState.objectMode = false;
+  }
+
+  _transform(bytesArr, encoding, cb) {
+    var buf = new Buffer(new Uint8Array(bytesArr));
+    this.push(buf);
+
+    cb();
+  }
+}
diff --git a/browser/libs/utils/exists.js b/browser/libs/utils/exists.js
new file mode 100644
index 0000000..9ea45b3
--- /dev/null
+++ b/browser/libs/utils/exists.js
@@ -0,0 +1,15 @@
+/*
+ * Given a collection of objects, returns true if all of them exist
+ * Returns false as soon as one does not exist.
+ * @param {*} [...] objects Objects to check existence of
+ * @return {bool} Whether all of the given objects exist or not
+ */
+export function exists(...objects) {
+  for (var obj of objects) {
+    if (typeof obj === 'undefined' || obj === null) {
+      return false;
+    }
+  }
+
+  return true;
+}
diff --git a/browser/libs/utils/formatting.js b/browser/libs/utils/formatting.js
new file mode 100644
index 0000000..29e64ec
--- /dev/null
+++ b/browser/libs/utils/formatting.js
@@ -0,0 +1,23 @@
+import { default as humanize } from 'npm:humanize'
+
+export function formatDate(d) {
+  if(d === undefined || d == null) { return; }
+  var naturalDay = humanize.naturalDay(d.getTime() / 1000);
+  var naturalTime = humanize.date('g:i a', d);
+  return naturalDay + ' at ' + naturalTime;
+}
+
+export function formatRelativeTime(d) {
+  if(d === undefined || d == null) { return; }
+  return humanize.relativeTime(d.getTime() / 1000);
+}
+
+export function formatInteger(n) {
+  if(n === undefined || n == null) { return; }
+  return humanize.numberFormat(n, 0);
+}
+
+export function formatBytes(b) {
+  if(b === undefined || b == null) { return; }
+  return humanize.filesize(b);
+}
diff --git a/browser/libs/utils/stream-byte-counter.js b/browser/libs/utils/stream-byte-counter.js
new file mode 100644
index 0000000..2a0b422
--- /dev/null
+++ b/browser/libs/utils/stream-byte-counter.js
@@ -0,0 +1,21 @@
+import { default as Stream } from "stream"
+
+var Transform = Stream.Transform;
+/*
+ * A through transform stream that counts number of bytes being piped to it
+ * @param {function} onUpdate Callback function that gets called with number of
+ * bytes read when a chunk is read
+ * @class
+ */
+export class StreamByteCounter extends Transform {
+  constructor(onUpdate) {
+    super();
+    this._onUpdate = onUpdate;
+  }
+
+  _transform(chunk, encoding, cb) {
+    this._onUpdate(chunk.length);
+    this.push(chunk)
+    cb();
+  }
+}
diff --git a/browser/libs/utils/stream-copy.js b/browser/libs/utils/stream-copy.js
new file mode 100644
index 0000000..1a99414
--- /dev/null
+++ b/browser/libs/utils/stream-copy.js
@@ -0,0 +1,60 @@
+import { default as Stream } from "stream"
+import { default as buffer } from "buffer"
+
+var Transform = Stream.Transform;
+var PassThrough = Stream.PassThrough;
+var Buffer = buffer.Buffer;
+/*
+ * A through transform stream keep a copy of the data piped to it and provides
+ * functions to create new copies of the stream on-demand
+ * @class
+ */
+export class StreamCopy extends Transform {
+  constructor() {
+    super();
+    this._writableState.objectMode = true;
+    this._readableState.objectMode = true;
+    // TODO(aghassemi) make this a FIFO buffer with reasonable max-size
+    this.buffer = [];
+    this.copies = [];
+    var self = this;
+    this.on('end', () => {
+      self.ended = true;
+      for (var i=0; i < self.copies.length; i++) {
+        self.copies[i].end();
+      }
+    });
+  }
+
+  _transform(chunk, encoding, cb) {
+    this.buffer.push(chunk);
+    this.push(chunk);
+    for (var i=0; i < this.copies.length; i++) {
+      this.copies[i].push(chunk);
+    }
+    cb();
+  }
+
+ /*
+  * Create a new copy of the stream
+  * @param {bool} onlyNewData Whether the copy should include
+  * existing data from the stream or just new data.
+  * @return {Stream} Copy of the stream
+  */
+  copy(onlyNewData) {
+    var copy = new PassThrough( { objectMode: true });
+    if (!onlyNewData) {
+      // copy existing data first in the order received
+      for (var i = 0; i < this.buffer.length; i++) {
+        copy.push(this.buffer[i]);
+      }
+    }
+    if (this.ended) {
+      copy.push(null);
+    } else {
+      this.copies.push(copy);
+    }
+
+    return copy;
+  }
+}
diff --git a/browser/libs/utils/stream-helpers.js b/browser/libs/utils/stream-helpers.js
new file mode 100644
index 0000000..5c2060c
--- /dev/null
+++ b/browser/libs/utils/stream-helpers.js
@@ -0,0 +1,7 @@
+import { default as es } from "npm:event-stream"
+
+export var streamUtil = {
+  split: es.split,
+  map: es.map,
+  writeArray: es.writeArray
+};
diff --git a/browser/libs/utils/url.js b/browser/libs/utils/url.js
new file mode 100644
index 0000000..d37e89f
--- /dev/null
+++ b/browser/libs/utils/url.js
@@ -0,0 +1,10 @@
+var hasProtocol = new RegExp('^(?:[a-z]+:)?//', 'i');
+
+/*
+ * Decides whether a string is an absolute Url by seeing if it starts with a protocol.
+ * @param {string} val string value to check.
+ * @return {bool} whether or not the string value is a Url
+ */
+export function isAbsoulteUrl(val) {
+  return hasProtocol.test(val);
+}
diff --git a/browser/libs/utils/web-component-loader.js b/browser/libs/utils/web-component-loader.js
new file mode 100644
index 0000000..225c93f
--- /dev/null
+++ b/browser/libs/utils/web-component-loader.js
@@ -0,0 +1,11 @@
+export function importComponent(path) {
+	return new Promise((resolve, reject) => {
+		var link = document.createElement('link');
+		link.setAttribute('rel', 'import');
+		link.setAttribute('href', path);
+		link.onload = function() {
+		  resolve();
+		};
+		document.body.appendChild(link);
+	});
+}
diff --git a/browser/package.json b/browser/package.json
new file mode 100644
index 0000000..24954fa
--- /dev/null
+++ b/browser/package.json
@@ -0,0 +1,15 @@
+{
+  "registry": "jspm",
+  "main": "app",
+  "directories": {
+    "jspmPackages": "third-party",
+    "lib": "lib",
+    "packages": "third-party"
+  },
+  "dependencies": {
+    "buffer": "^0.1.0",
+    "npm:event-stream": "npm:event-stream@^3.1.5",
+    "npm:humanize": "npm:humanize@^0.0.9",
+    "stream": "^0.1.0"
+  }
+}
diff --git a/browser/pipe-viewers/builtin/common/common.css b/browser/pipe-viewers/builtin/common/common.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/browser/pipe-viewers/builtin/common/common.css
diff --git a/browser/pipe-viewers/builtin/console/component.css b/browser/pipe-viewers/builtin/console/component.css
new file mode 100644
index 0000000..d1ef530
--- /dev/null
+++ b/browser/pipe-viewers/builtin/console/component.css
@@ -0,0 +1,26 @@
+:host {
+  background-color: #000000;
+  height: 100%;
+  display: block;
+  overflow: auto;
+}
+
+pre {
+  margin: 0;
+  padding: 0.5em;
+
+  font-family: Fixed, monospace;
+  line-height: 1.2em;
+  word-break: break-word;
+
+  color: #ffffff;
+}
+
+.auto-scroll {
+  position: fixed;
+  right: 40px;
+  bottom: 0;
+  opacity: 0.8;
+  padding: 0.8em;
+  background-color: #ffeb3b;
+}
diff --git a/browser/pipe-viewers/builtin/console/component.html b/browser/pipe-viewers/builtin/console/component.html
new file mode 100644
index 0000000..5505f9b
--- /dev/null
+++ b/browser/pipe-viewers/builtin/console/component.html
@@ -0,0 +1,57 @@
+<link rel="import" href="../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../third-party/paper-checkbox/paper-checkbox.html">
+
+<polymer-element name="p2b-plugin-console">
+  <template>
+    <link rel="stylesheet" href="component.css">
+    <link rel="stylesheet" href="../../../libs/css/common-style.css">
+    <div title="Auto Scroll" class="auto-scroll {{ {hidden : !scrolling} | tokenList}}">
+      <paper-checkbox checked="{{autoScroll}}" label="Auto Scroll" id="autoscroll"></paper-checkbox>
+    </div>
+    <pre id="console"></pre>
+  </template>
+  <script>
+    Polymer('p2b-plugin-console', {
+      ready: function() {
+        this.textBuffer = [];
+        this.autoScroll = true;
+      },
+
+      attached: function() {
+        this.renderLoop();
+      },
+      detached: function() {
+        this.isDetached = true;
+      },
+      renderLoop: function() {
+        var self = this;
+        if (!this.isDetached) {
+          requestAnimationFrame(function() {
+            self.render();
+            self.renderLoop();
+          });
+        }
+      },
+      render: function() {
+        if (this.textBuffer.length === 0) {
+          return;
+        }
+        var textNode = document.createTextNode(this.textBuffer.join(''));
+        this.textBuffer = [];
+        this.$.console.appendChild(textNode);
+        var scrollTop = this.scrollTop;
+        this.scrolling =  scrollTop > 0;
+        if (this.autoScroll) {
+          this.scrollTop = this.scrollHeight;
+        }
+      },
+      /*
+       * Appends text to the console
+       * @param {string} text Text to add
+       */
+      addText: function(text) {
+        this.textBuffer.push(text);
+      }
+    });
+    </script>
+</polymer-element>
diff --git a/browser/pipe-viewers/builtin/console/panic/component.css b/browser/pipe-viewers/builtin/console/panic/component.css
new file mode 100644
index 0000000..5690279
--- /dev/null
+++ b/browser/pipe-viewers/builtin/console/panic/component.css
@@ -0,0 +1,46 @@
+:host {
+  background-color: #000000;
+  height: 100%;
+  display: block;
+  overflow: auto;
+}
+
+pre {
+  margin: 0;
+  padding: 0.5em;
+
+  font-family: Fixed, monospace;
+  line-height: 1.2em;
+  word-break: break-word;
+
+  color: #ffffff;
+}
+
+.auto-scroll {
+  position: fixed;
+  right: 40px;
+  bottom: 0;
+  opacity: 0.8;
+  padding: 0.8em;
+  background-color: #ffeb3b;
+  z-index: 1;
+}
+
+paper-input {
+  position: fixed;
+  right: 50px;
+  color: #cccccc;
+  background-color: #ffffff;
+  z-index: 1;
+}
+
+core-item.showHide {
+  background-color: #ff0000;
+  color: #ffffff;
+  cursor: pointer;
+  margin: 5px;
+}
+
+.yellowback {
+  background-color: #666600;
+}
diff --git a/browser/pipe-viewers/builtin/console/panic/component.html b/browser/pipe-viewers/builtin/console/panic/component.html
new file mode 100644
index 0000000..6669be4
--- /dev/null
+++ b/browser/pipe-viewers/builtin/console/panic/component.html
@@ -0,0 +1,122 @@
+<link rel="import" href="../../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../../third-party/paper-checkbox/paper-checkbox.html">
+
+<polymer-element name="p2b-plugin-console-panic">
+  <template>
+    <link rel="stylesheet" href="component.css">
+    <link rel="stylesheet" href="../../../../libs/css/common-style.css">
+    <div title="Auto Scroll" class="auto-scroll {{ {hidden : !scrolling} | tokenList}}">
+      <paper-checkbox checked="{{autoScroll}}" label="Auto Scroll" id="autoscroll"></paper-checkbox>
+    </div>
+    <paper-input id="filter" label="Filter by keyword..."></paper-input>
+    <pre id="console"></pre>
+  </template>
+  <script>
+    Polymer('p2b-plugin-console-panic', {
+      ready: function() {
+        this.autoScroll = true;
+
+        // Prepare our current container and current line
+        this.startNewContainer(false);
+        this.startNewLine();
+
+        this.$.filter.addEventListener('input', this.filterAllText.bind(this), false);
+      },
+      /*
+       * Scrolls the plugin console, if this.autoScroll is true
+       */
+      scrollAuto: function() {
+        // Scroll if we need to.
+        var scrollTop = this.scrollTop;
+        this.scrolling =  scrollTop > 0;
+        if (this.autoScroll) {
+          this.scrollTop = this.scrollHeight;
+        }
+      },
+      /*
+       * Turns off autoscrolling
+       */
+      scrollOff: function() {
+        this.autoScroll = false;
+      },
+      /*
+       * Create a new container div for lines of text
+       * @param {boolean} whether or not the container should start hidden or not
+       * @param {string} (optional) the control button's name for toggling hidden on this container.
+       */
+      startNewContainer: function(isCollapsed, withToggleButtonName) {
+        this.container = document.createElement('div');
+
+        // If the container should be collapsed, hide it.
+        if (isCollapsed) {
+          this.container.classList.toggle('hidden');
+        }
+
+        if (withToggleButtonName) {
+          // This button controls whether to show/hide the container.
+          var control = document.createElement('core-item');
+          control.label = withToggleButtonName;
+          control.classList.add("showHide");
+          control.icon = "swap-vert";
+          var container = this.container;
+          control.addEventListener(
+            'click',
+            function toggle() {
+              container.classList.toggle('hidden');
+            },
+            false
+          );
+          this.$.console.appendChild(control);
+        }
+
+        this.$.console.appendChild(this.container);
+      },
+      /*
+       * Create a new div for the current line of text. Scroll if necessary.
+       */
+      startNewLine: function() {
+        this.line = document.createElement('div');
+        this.line.classList.add('line');
+        this.container.appendChild(this.line);
+        this.scrollAuto();
+      },
+      /*
+       * Add a <br> element to the container. Scroll if necessary.
+       */
+      addLineBreak: function() {
+        this.container.appendChild(document.createElement('br'));
+        this.scrollAuto();
+      },
+      /*
+       * Append text to the current line of text.
+       * Additionally, filter this line since it was modified.
+       * @param {string} the text to be appended
+       */
+      addText: function(text) {
+        this.line.appendChild(document.createTextNode(text));
+        this.filter(this.line);
+      },
+      /*
+       * All elements with class 'line' are filtered.
+       */
+      filterAllText: function() {
+        var elems = this.$.console.getElementsByClassName('line');
+        for (var i = 0; i < elems.length; i++) {
+          this.filter(elems[i]);
+        }
+      },
+      /*
+       * If the given element's text contains the filter's input value,
+       * a yellow background is assigned. Otherwise, it is removed.
+       * @param {div} the given element whose background will be modified
+       */
+      filter: function(elem) {
+        var value = this.$.filter.inputValue;
+
+        // Force add/remove the yellowback class depending on if the element has the value or not. Don't highlight if the value is empty.
+        var shouldHighlight = (value && elem.textContent.indexOf(value) >= 0);
+        elem.classList.toggle('yellowback', shouldHighlight);
+      }
+    });
+    </script>
+</polymer-element>
diff --git a/browser/pipe-viewers/builtin/console/panic/plugin.js b/browser/pipe-viewers/builtin/console/panic/plugin.js
new file mode 100644
index 0000000..2084c18
--- /dev/null
+++ b/browser/pipe-viewers/builtin/console/panic/plugin.js
@@ -0,0 +1,68 @@
+/*
+ * Console is a Pipe Viewer that displays a text stream as unformatted text
+ * @tutorial echo "Hello World" | p2b google/p2b/[name]/console/panic
+ * @fileoverview
+ */
+
+import { View } from 'view';
+import { PipeViewer } from 'pipe-viewer';
+
+class ConsolePanicPipeViewer extends PipeViewer {
+  get name() {
+    return 'console-panic';
+  }
+
+  play(stream) {
+    var consoleView = document.createElement('p2b-plugin-console-panic');
+
+    // Internal stream variables
+    var panicked = false;
+    var lastPiece = "";
+
+    // read data as UTF8
+    stream.setEncoding('utf8');
+    stream.on('data', (buf) => {
+      // Always split the incoming text by newline.
+      var pieces = buf.toString().split("\n");
+      for (var i = 0; i < pieces.length; i++) {
+        var piece = pieces[i];
+
+        // flags for the status of the current line
+        var goroutine = (piece.search(/goroutine \d+ \[\D+\]:/) == 0);
+        var startOfLine = (i != 0 || lastPiece == "");
+
+        if (goroutine && startOfLine) {
+          // We spotted a goroutine at the start of a line, so create a new container.
+          consoleView.startNewContainer(panicked, piece);
+
+          // Stop scrolling if we've already panicked.
+          // This will focus the element on the first crashed goroutine.
+          if (panicked) {
+            consoleView.scrollAuto();
+            consoleView.scrollOff();
+          }
+          panicked = true;
+
+          // We also need a new line for our new container.
+          consoleView.startNewLine();
+        } else if (i > 0) {
+          // These lines followed a \n in the buffer and thus should go on a new line.
+          if (lastPiece == "") {
+            // The previous line was empty; empty div's have 0 height.
+            // We need to add a <br /> to print the linebreak properly.
+            consoleView.addLineBreak();
+          }
+          consoleView.startNewLine();
+        }
+
+        // Add the relevant text.
+        consoleView.addText(piece);
+        lastPiece = piece;
+      }
+    });
+
+    return new View(consoleView);
+  }
+}
+
+export default ConsolePanicPipeViewer;
diff --git a/browser/pipe-viewers/builtin/console/plugin.js b/browser/pipe-viewers/builtin/console/plugin.js
new file mode 100644
index 0000000..a1461ce
--- /dev/null
+++ b/browser/pipe-viewers/builtin/console/plugin.js
@@ -0,0 +1,29 @@
+/*
+ * Console is a Pipe Viewer that displays a text stream as unformatted text
+ * @tutorial echo "Hello World" | p2b google/p2b/[name]/console
+ * @fileoverview
+ */
+
+import { View } from 'view';
+import { PipeViewer } from 'pipe-viewer';
+
+class ConsolePipeViewer extends PipeViewer {
+  get name() {
+    return 'console';
+  }
+
+  play(stream) {
+    var consoleView = document.createElement('p2b-plugin-console');
+
+    // read data as UTF8
+    stream.setEncoding('utf8');
+    stream.on('data', (buf) => {
+      var textVal = buf.toString();
+      consoleView.addText(textVal);
+    });
+
+    return new View(consoleView);
+  }
+}
+
+export default ConsolePipeViewer;
diff --git a/browser/pipe-viewers/builtin/dev/null/plugin.js b/browser/pipe-viewers/builtin/dev/null/plugin.js
new file mode 100644
index 0000000..0fcec76
--- /dev/null
+++ b/browser/pipe-viewers/builtin/dev/null/plugin.js
@@ -0,0 +1,33 @@
+/*
+ * dev/null simply consumes the stream without taking any action on the data
+ * or keeping it in memory
+ * @tutorial echo "To the black hole!" | p2b google/p2b/[name]/dev/null
+ * @fileoverview
+ */
+
+import { View } from 'view';
+import { PipeViewer } from 'pipe-viewer';
+
+class DevNullPipeViewer extends PipeViewer {
+  get name() {
+    return 'dev/null';
+  }
+
+  play(stream) {
+
+    var blackhole = document.createElement('p2b-blackhole');
+    blackhole.start();
+
+    stream.on('data', () => {
+      // consume the stream
+    });
+
+    stream.on('end', () => {
+      blackhole.stop();
+    });
+
+    return new View(blackhole);
+  }
+}
+
+export default DevNullPipeViewer;
diff --git a/browser/pipe-viewers/builtin/dev/null/text/plugin.js b/browser/pipe-viewers/builtin/dev/null/text/plugin.js
new file mode 100644
index 0000000..b70e1e1
--- /dev/null
+++ b/browser/pipe-viewers/builtin/dev/null/text/plugin.js
@@ -0,0 +1,33 @@
+/*
+ * similar to dev/null, it consumes the stream without keeping it in memory but
+ * it tried to decode the steaming bytes as UTF-8 string first
+ * @tutorial echo "To the black hole!" | p2b google/p2b/[name]/dev/null/text
+ * @fileoverview
+ */
+
+import { View } from 'view';
+import { PipeViewer } from 'pipe-viewer';
+import { redirectPlay } from 'pipe-viewer-delegation';
+
+class DevNullTextPipeViewer extends PipeViewer {
+  get name() {
+    return 'dev/null/text';
+  }
+
+  play(stream) {
+
+    stream.setEncoding('utf8');
+
+    stream.on('data', (buf) => {
+      // consume the stream as string
+      var text = buf.toString();
+    });
+
+    // redirect to regular dev/null to play the stream
+    var delegatedView = redirectPlay('dev/null', stream);
+
+    return delegatedView;
+  }
+}
+
+export default DevNullTextPipeViewer;
diff --git a/browser/pipe-viewers/builtin/git/status/component.css b/browser/pipe-viewers/builtin/git/status/component.css
new file mode 100644
index 0000000..99617a1
--- /dev/null
+++ b/browser/pipe-viewers/builtin/git/status/component.css
@@ -0,0 +1,29 @@
+::shadow /deep/ .state-icon.notstaged {
+  fill: #f57c00;
+}
+
+::shadow /deep/ .state-icon.staged {
+  fill: #689f38;
+}
+
+::shadow /deep/ .state-icon.conflicted {
+  fill: #bf360c;
+  -webkit-animation: blink 0.6s infinite alternate;
+}
+
+::shadow /deep/ .state-icon.untracked {
+  fill: #bf360c;
+}
+
+::shadow /deep/ .state-icon.ignored {
+  fill: #689f38;
+}
+
+::shadow /deep/ .action-icon {
+  fill: #91a7ff;
+}
+
+::shadow /deep/ .file-parent {
+  font-size: 0.8em;
+  color: rgba(0,0,0,.54);
+}
diff --git a/browser/pipe-viewers/builtin/git/status/component.html b/browser/pipe-viewers/builtin/git/status/component.html
new file mode 100644
index 0000000..6855373
--- /dev/null
+++ b/browser/pipe-viewers/builtin/git/status/component.html
@@ -0,0 +1,68 @@
+<link rel="import" href="../../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../../third-party/core-icon/core-icon.html">
+<link rel="import" href="../../../../libs/ui-components/data-grid/grid/component.html">
+<link rel="import" href="../../../../libs/ui-components/data-grid/grid/column/component.html">
+<link rel="import" href="../../../../libs/ui-components/data-grid/filter/select/component.html">
+<link rel="import" href="../../../../libs/ui-components/data-grid/filter/select/item/component.html">
+<link rel="import" href="../../../../libs/ui-components/data-grid/filter/toggle/component.html">
+<link rel="import" href="../../../../libs/ui-components/data-grid/search/component.html">
+
+<polymer-element name="p2b-plugin-git-status">
+  <template>
+
+    <link rel="stylesheet" href="../../../../libs/css/common-style.css">
+    <link rel="stylesheet" href="component.css">
+
+    <p2b-grid id="grid" defaultSortKey="state" defaultSortAscending dataSource="{{ dataSource }}" summary="Data Grid displaying status of modified file for the git repository.">
+
+      <!-- Search -->
+      <p2b-grid-search label="Search Logs"></p2b-grid-search>
+
+      <!-- State Filter (multiple allowed) -->
+      <p2b-grid-filter-select multiple key="state" label="Show state">
+        <p2b-grid-filter-select-item checked label="Staged" value="staged"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Not Staged" value="notstaged"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Conflicted" value="conflicted"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Untracked" value="untracked"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Ignored" value="ignored"></p2b-grid-filter-select-item>
+      </p2b-grid-filter-select>
+
+      <!-- Action Filter (multiple allowed) -->
+      <p2b-grid-filter-select multiple key="action" label="Show actions">
+        <p2b-grid-filter-select-item checked label="Added" value="added"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Deleted" value="deleted"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Modified" value="modified"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Renamed" value="renamed"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Copied" value="copied"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Unknown" value="unknown"></p2b-grid-filter-select-item>
+      </p2b-grid-filter-select>
+
+      <!-- Columns, sorting and cell templates -->
+      <p2b-grid-column label="State" key="state" sortable flex="2" priority="2" >
+        <template>
+          <core-icon class="state-icon {{ item.state }}" icon="{{ item.stateIcon }}" title="{{item.state}}"></core-icon>
+          <span moreInfoOnly style="vertical-align:middle">{{item.state}}</span>
+        </template>
+      </p2b-grid-column>
+      <p2b-grid-column label="Action" key="action" sortable flex="2" priority="3" >
+        <template>
+          <core-icon class="action-icon {{ item.action }}" icon="{{ item.actionIcon }}" title="{{item.action}}"></core-icon>
+          <span moreInfoOnly style="vertical-align:middle">{{item.action}}</span>
+        </template>
+      </p2b-grid-column>
+      <p2b-grid-column label="File" key="filename" sortable primary flex="8" minFlex="5" priority="1" >
+        <template>{{ item.filename }}
+          <div class="file-parent" title="folder: {{item.fileParent}}">{{ item.fileParent }}</div>
+        </template>
+      </p2b-grid-column>
+      <p2b-grid-column label="Summary" flex="7" minFlex="3" priority="4" >
+        <template>{{ item.summary }}</template>
+      </p2b-grid-column>
+
+    </p2b-grid>
+  </template>
+  <script>
+    Polymer('p2b-plugin-git-status', {
+    });
+    </script>
+</polymer-element>
diff --git a/browser/pipe-viewers/builtin/git/status/data-source.js b/browser/pipe-viewers/builtin/git/status/data-source.js
new file mode 100644
index 0000000..1b0b043
--- /dev/null
+++ b/browser/pipe-viewers/builtin/git/status/data-source.js
@@ -0,0 +1,51 @@
+/*
+ * Implement the data source which handles searching, filtering
+ * and sorting of the git status items
+ * @fileoverview
+ */
+
+import { gitStatusSort } from './sorter';
+import { gitStatusSearch } from './searcher';
+import { gitStatusFilter } from './filterer';
+
+export class gitStatusDataSource {
+  constructor(items) {
+
+    /*
+     * all items, unlimited buffer for now.
+     * @private
+     */
+    this.allItems = items;
+  }
+
+  /*
+   * Implements the fetch method expected by the grid components.
+   * handles searching, filtering and sorting of the data.
+   * search, sort and filters are provided by the grid control whenever they are
+   * changed by the user.
+   * DataSource is called automatically by the grid when user interacts with the component
+   * Grid does some batching of user actions and only calls fetch when needed.
+   * keys provided for sort and filters correspond to keys set in the markup
+   * when constructing the grid.
+   * @param {object} search search{key<string>} current search keyword
+   * @param {object} sort sort{key<string>, ascending<bool>} current sort key and direction
+   * @param {map} filters map{key<string>, values<Array>} Map of filter keys to currently selected filter values
+   * @return {Array<object>} Returns an array of filtered sorted results of the items.
+   */
+  fetch(search, sort, filters) {
+
+    var filteredSortedItems = this.allItems.
+      filter((item) => {
+        return gitStatusFilter(item, filters);
+      }).
+      filter((item) => {
+        return gitStatusSearch(item, search.keyword);
+      }).
+      sort((item1, item2) => {
+        return gitStatusSort(item1, item2, sort.key, sort.ascending);
+      });
+
+    return filteredSortedItems;
+
+  }
+}
diff --git a/browser/pipe-viewers/builtin/git/status/filterer.js b/browser/pipe-viewers/builtin/git/status/filterer.js
new file mode 100644
index 0000000..d086ca8
--- /dev/null
+++ b/browser/pipe-viewers/builtin/git/status/filterer.js
@@ -0,0 +1,43 @@
+/*
+ * Returns whether the given git status items matches the map of filters.
+ * @param {Object} item A single git status item as defined by parser.item
+ * @param {map} filters Map of keys to selected filter values as defined
+ * when constructing the filters in the grid components.
+ * e.g. filters:{'state':['staged'], 'action':['added','modified']}
+ * @return {boolean} Whether the item satisfies ALL of the given filters.
+ */
+export function gitStatusFilter(item, filters) {
+  if (Object.keys(filters).length === 0) {
+    return true;
+  }
+
+  for (var key in filters) {
+    var isMatch = applyFilter(item, key, filters[key]);
+    // we AND all the filters, short-circuit for early termination
+    if (!isMatch) {
+      return false;
+    }
+  }
+
+  // matches all filters
+  return true;
+};
+
+/*
+ * Returns whether the given git status item matches a single filter
+ * @param {Object} item A single git status item as defined by parser.item
+ * @param {string} key filter key e.g. 'state'
+ * @param {string} value filter value e.g. '['staged','untracked']
+ * @return {boolean} Whether the item satisfies then the given filter key value pair
+ * @private
+ */
+function applyFilter(item, key, value) {
+  switch (key) {
+    case 'state':
+    case 'action':
+      return value.indexOf(item[key]) >= 0;
+    default:
+      // ignore unknown filters
+      return true;
+  }
+}
diff --git a/browser/pipe-viewers/builtin/git/status/parser.js b/browser/pipe-viewers/builtin/git/status/parser.js
new file mode 100644
index 0000000..b9e2c50
--- /dev/null
+++ b/browser/pipe-viewers/builtin/git/status/parser.js
@@ -0,0 +1,170 @@
+/*
+ * Parse utilities for git statuses
+ * @see http://git-scm.com/docs/git-status#_short_format
+ * @fileoverview
+ */
+
+/*
+ * Parses a single line of text produced by git status --short command
+ * into an structured object representing the status of that file.
+ * git-status short format is XY PATH1 -> PATH2
+ * Please see documentation at:
+ * http://git-scm.com/docs/git-status#_short_format
+ * for details of the format and meaning of each component/
+ * @param {string} gitStatusLine A single line of text produced
+ * by git status --short command
+ * @return {parser.item} A parsed object containing status, file path, etc..
+ */
+export function parse(gitStatusLine) {
+
+  var validGitStatusRegex =  /^[\sMADRCU?!]{2}[\s]{1}.+/
+  if(!validGitStatusRegex.test(gitStatusLine)) {
+    throw new Error('Invalid git status line format. ' + gitStatusLine +
+      ' does not match XY PATH1 -> PATH2 format where -> PATH2 is optional ' +
+      ' and X and Y should be one of [<space>MADRCU?!].' +
+      ' Please ensure you are using --short flag when running git status');
+  }
+
+  // X is status for working tree
+  // Y is status for the index
+  var X = gitStatusLine.substr(0,1);
+  var Y = gitStatusLine.substr(1,1);
+
+  // files
+  var files = gitStatusLine.substring(3).split('->');
+  var file = files[0];
+  var oldFile = null;
+  // we may have oldFile -> file for cases like rename and copy
+  if (files.length == 2) {
+    file = files[1];
+    oldFile = files[0];
+  }
+
+  var fileStateInfo = getFileState(X, Y);
+  var fileAction = getFileAction(X, Y , fileStateInfo.state);
+  var summary = fileStateInfo.summary + ', file ' + fileAction;
+  if(oldFile) {
+    summary += ' to ' + oldFile;
+  }
+
+  return new item(
+    fileAction,
+    fileStateInfo.state,
+    file,
+    summary
+  );
+}
+
+/*
+ * A structure representing the status of a git file.
+ * @param {string} action, one of added, deleted, renamed, copied, modified, unknown
+ * @param {string} state, one staged, notstaged, conflicted, untracked, ignored
+ * @param {string} file filename and path
+ * @param {string} summary A summary text for what these states mean
+ * @class
+ * @private
+ */
+class item {
+  constructor(action, state, file, summary) {
+    this.action = action;
+    this.state = state;
+    this.file = file;
+    this.summary = summary;
+  }
+}
+
+var actionCodes = {
+  'M': 'modified',
+  'U': 'modified',
+  'A': 'added',
+  'D': 'deleted',
+  'R': 'renamed',
+  'C': 'copied',
+  '?': 'unknown',
+  '!': 'unknown'
+}
+/*
+ * Returns the action performed on the file, one of added, deleted, renamed,
+ * copied, modified, unknown
+ * @private
+ */
+function getFileAction(X, Y, fileState) {
+  var codeToUse = X;
+  if (fileState === 'notstaged') {
+    codeToUse = Y;
+  }
+
+  return actionCodes[codeToUse];
+}
+
+/*
+ * Returns the git state of file, staged, notstaged, conflicted, untracked, ignored
+ * @private
+ */
+function getFileState(X, Y) {
+
+  // check for conflict, the following combinations represent a conflict
+  // ------------------------------------------------
+  //     D           D    unmerged, both deleted
+  //     A           U    unmerged, added by us
+  //     U           D    unmerged, deleted by them
+  //     U           A    unmerged, added by them
+  //     D           U    unmerged, deleted by us
+  //     A           A    unmerged, both added
+  //     U           U    unmerged, both modified
+  // -------------------------------------------------
+  var conflictSummary = null;
+  if(X === 'D' && Y == 'D') {
+    conflictSummary = 'Conflicted, both deleted';
+  } else if(X === 'A' && Y === 'U') {
+    conflictSummary = 'Conflicted, added by us';
+  } else if(X === 'U' && Y === 'D') {
+    conflictSummary = 'Conflicted, deleted by them';
+  } else if(X === 'U' && Y === 'A') {
+    conflictSummary = 'Conflicted, added by them';
+  } else if(X === 'D' && Y === 'U') {
+    conflictSummary = 'Conflicted, deleted by us';
+  } else if(X === 'A' && Y === 'A') {
+    conflictSummary = 'Conflicted, both added';
+  } else if(X === 'U' && Y === 'U') {
+    conflictSummary = 'Conflicted, both modified';
+  }
+
+  if (conflictSummary !== null) {
+    return {
+      state: 'conflicted',
+      summary: conflictSummary
+    }
+  }
+
+  // check for untracked
+  if (X === '?' || Y === '?') {
+    return {
+      state:'untracked',
+      summary: 'Untracked file'
+    }
+  }
+
+  // check for ignored
+  if (X === '!' || Y === '!') {
+    return {
+      state: 'ignored',
+      summary: 'Ignored file'
+    }
+  }
+
+  // check for notstaged
+  if (Y !== ' ') {
+    return {
+      state:'notstaged',
+      summary: 'Not staged for commit'
+    }
+  } else {
+    // otherwise staged
+    return {
+      state: 'staged',
+      summary: 'Staged for commit'
+    }
+  }
+}
+
diff --git a/browser/pipe-viewers/builtin/git/status/plugin.js b/browser/pipe-viewers/builtin/git/status/plugin.js
new file mode 100644
index 0000000..e08b942
--- /dev/null
+++ b/browser/pipe-viewers/builtin/git/status/plugin.js
@@ -0,0 +1,158 @@
+/*
+ * git/status is a Pipe Viewer that displays output of "git status --short"
+ * in a graphical grid.
+ * Only supported with Git's --short  flag.
+ * @tutorial git status --short | p2b google/p2b/[name]/git/status
+ * @fileoverview
+ */
+import { View } from 'view';
+import { PipeViewer } from 'pipe-viewer';
+import { streamUtil } from 'stream-helpers';
+import { Logger } from 'logger';
+import { parse } from './parser';
+import { gitStatusDataSource } from './data-source';
+
+var log = new Logger('pipe-viewers/builtin/git/status');
+
+class GitStatusPipeViewer extends PipeViewer {
+  get name() {
+    return 'git/status';
+  }
+
+  play(stream) {
+    stream.setEncoding('utf8');
+
+    // split by new line
+    stream = stream.pipe(streamUtil.split(/\r?\n/));
+
+    // parse the git status items
+    stream = stream.pipe(streamUtil.map((line, cb) => {
+      if (line.trim() === '') {
+        // eliminate the item
+        cb();
+        return;
+      }
+      var item;
+      try {
+        item = parse(line);
+      } catch(e) {
+        log.debug(e);
+      }
+      if (item) {
+        addAdditionalUIProperties(item);
+        cb(null, item);
+      } else {
+        // eliminate the item
+        cb();
+      }
+    }));
+
+    // we return a view promise instead of a view since we want to wait
+    // until all items arrive before showing the data.
+    var viewPromise = new Promise(function(resolve,reject) {
+      // write into an array when stream is done return the UI component
+      stream.pipe(streamUtil.writeArray((err, items) => {
+        if (err) {
+          reject(err);
+        } else {
+          var statusView = document.createElement('p2b-plugin-git-status');
+          statusView.dataSource = new gitStatusDataSource(items);
+          resolve(new View(statusView));
+        }
+      }));
+    });
+
+    return viewPromise;
+  }
+}
+
+/*
+ * Adds additional UI specific properties to the item
+ * @private
+ */
+function addAdditionalUIProperties(item) {
+  addActionIconProperty(item);
+  addStateIconProperty(item);
+  addFileNameFileParentProperty(item);
+}
+
+/*
+ * Adds an icon property to the item specifying what icon to display
+ * based on state
+ * @private
+ */
+function addStateIconProperty(item) {
+  var iconName;
+  switch (item.state) {
+    case 'staged':
+      iconName = 'check-circle';
+      break;
+    case 'notstaged':
+      iconName = 'warning';
+      break;
+    case 'conflicted':
+      iconName = 'block';
+      break;
+    case 'untracked':
+      iconName = 'error';
+      break;
+    case 'ignored':
+      iconName = 'visibility-off';
+      break;
+  }
+
+  item.stateIcon = iconName;
+}
+
+/*
+ * Adds an icon property to the item specifying what icon to display
+ * based on action
+ * @private
+ */
+function addActionIconProperty(item) {
+  var iconName;
+  switch (item.action) {
+    case 'added':
+      iconName = 'add';
+      break;
+    case 'deleted':
+      iconName = 'clear';
+      break;
+    case 'modified':
+      iconName = 'translate';
+      break;
+    case 'renamed':
+      iconName = 'swap-horiz';
+      break;
+    case 'copied':
+      iconName = 'content-copy';
+      break;
+    case 'unknown':
+      iconName = 'remove';
+      break;
+  }
+
+  item.actionIcon = iconName;
+}
+
+/*
+ * Splits file into filename and fileParent
+ * @private
+ */
+function addFileNameFileParentProperty(item) {
+
+  var filename = item.file;
+  var fileParent = "./";
+
+  var slashIndex = item.file.lastIndexOf('/');
+
+  if (slashIndex > 0) {
+    filename = item.file.substr(slashIndex + 1);
+    fileParent = item.file.substring(0, slashIndex);
+  }
+
+  item.filename = filename;
+  item.fileParent = fileParent;
+}
+
+export default GitStatusPipeViewer;
diff --git a/browser/pipe-viewers/builtin/git/status/searcher.js b/browser/pipe-viewers/builtin/git/status/searcher.js
new file mode 100644
index 0000000..204bb92
--- /dev/null
+++ b/browser/pipe-viewers/builtin/git/status/searcher.js
@@ -0,0 +1,12 @@
+export function gitStatusSearch(item, keyword) {
+  if (!keyword) {
+    return true;
+  }
+
+  // we only search file
+  if (item.file.indexOf(keyword) >= 0) {
+    return true
+  }
+
+  return false;
+};
diff --git a/browser/pipe-viewers/builtin/git/status/sorter.js b/browser/pipe-viewers/builtin/git/status/sorter.js
new file mode 100644
index 0000000..80fee35
--- /dev/null
+++ b/browser/pipe-viewers/builtin/git/status/sorter.js
@@ -0,0 +1,41 @@
+var stateSortPriority = {
+  'conflicted' : 1,
+  'untracked' : 2,
+  'notstaged' : 3,
+  'staged': 4,
+  'ignored': 5
+}
+
+var actionSortPriority = {
+  'added' : 1,
+  'deleted' : 2,
+  'modified' : 3,
+  'renamed': 4,
+  'copied': 5,
+  'unknown': 6
+}
+
+export function gitStatusSort(item1, item2, key, ascending) {
+  var first = item1[key];
+  var second = item2[key];
+  if (!ascending) {
+    first = item2[key];
+    second = item1[key];
+  }
+
+  if (key === 'state') {
+    first = stateSortPriority[first];
+    second = stateSortPriority[second];
+  }
+
+  if (key === 'action') {
+    first = actionSortPriority[first];
+    second = actionSortPriority[second];
+  }
+
+  if (typeof first === 'string') {
+    return first.localeCompare(second);
+  } else {
+    return first - second;
+  }
+};
diff --git a/browser/pipe-viewers/builtin/image/plugin.js b/browser/pipe-viewers/builtin/image/plugin.js
new file mode 100644
index 0000000..5f1aaef
--- /dev/null
+++ b/browser/pipe-viewers/builtin/image/plugin.js
@@ -0,0 +1,35 @@
+/*
+ * Image is a Pipe Viewer that displays an image
+ * @tutorial cat image.jpg | p2b google/p2b/[name]/image
+ * @fileoverview
+ */
+
+import { View } from 'view';
+import { PipeViewer } from 'pipe-viewer';
+
+class ImagePipeViewer extends PipeViewer {
+  get name() {
+    return 'image';
+  }
+
+  play(stream) {
+    var viewPromise = new Promise(function(resolve,reject) {
+      var blobParts = [];
+      stream.on('data', (chunk) => {
+        blobParts.push(new Uint8Array(chunk));
+      });
+
+      stream.on('end', () => {
+        var image = document.createElement('img');
+        image.style.width = '100%';
+        var url = URL.createObjectURL(new Blob(blobParts));
+        image.src = url;
+        resolve(new View(image));
+      });
+    });
+
+    return viewPromise;
+  }
+}
+
+export default ImagePipeViewer;
diff --git a/browser/pipe-viewers/builtin/vlog/component.css b/browser/pipe-viewers/builtin/vlog/component.css
new file mode 100644
index 0000000..cfbfeee
--- /dev/null
+++ b/browser/pipe-viewers/builtin/vlog/component.css
@@ -0,0 +1,29 @@
+::shadow /deep/ .line-number::before {
+  content: '#';
+}
+::shadow /deep/ .line-number {
+  color: rgb(51, 103, 214);
+  margin-left: 0.1em;
+  font-size: 0.8em;
+}
+
+::shadow /deep/ .message-text {
+  word-break: break-word;
+}
+
+::shadow /deep/ .level-icon.error {
+  fill: #e51c23;
+}
+
+::shadow /deep/ .level-icon.warning {
+  fill: #f57c00;
+}
+
+::shadow /deep/ .level-icon.info {
+  fill: #689f38;
+}
+
+::shadow /deep/ .level-icon.fatal {
+  fill: #bf360c;
+  -webkit-animation: blink 0.6s infinite alternate;
+}
diff --git a/browser/pipe-viewers/builtin/vlog/component.html b/browser/pipe-viewers/builtin/vlog/component.html
new file mode 100644
index 0000000..3a70a5c
--- /dev/null
+++ b/browser/pipe-viewers/builtin/vlog/component.html
@@ -0,0 +1,72 @@
+<link rel="import" href="../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../third-party/core-icon/core-icon.html">
+<link rel="import" href="../../../libs/ui-components/data-grid/grid/component.html">
+<link rel="import" href="../../../libs/ui-components/data-grid/grid/column/component.html">
+<link rel="import" href="../../../libs/ui-components/data-grid/filter/select/component.html">
+<link rel="import" href="../../../libs/ui-components/data-grid/filter/select/item/component.html">
+<link rel="import" href="../../../libs/ui-components/data-grid/filter/toggle/component.html">
+<link rel="import" href="../../../libs/ui-components/data-grid/search/component.html">
+
+<polymer-element name="p2b-plugin-vlog">
+  <template>
+    <link rel="stylesheet" href="../../../libs/css/common-style.css">
+    <link rel="stylesheet" href="component.css">
+
+    <p2b-grid id="grid" defaultSortKey="date" dataSource="{{ dataSource }}" summary="Data Grid displaying veyron log items in a tabular format with filters and search options.">
+
+      <!-- Search -->
+      <p2b-grid-search label="Search Logs"></p2b-grid-search>
+
+      <!-- Filter to select log level (multiple allowed) -->
+      <p2b-grid-filter-select multiple key="level" label="Show levels">
+        <p2b-grid-filter-select-item checked label="Fatal" value="fatal"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Error" value="error"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Warning" value="warning"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item checked label="Info" value="info"></p2b-grid-filter-select-item>
+      </p2b-grid-filter-select>
+
+      <!-- Filter to select date range -->
+      <p2b-grid-filter-select key="date" label="Logs since">
+        <p2b-grid-filter-select-item checked label="Any time" value="all"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item label="Past 1 hour" value="1"></p2b-grid-filter-select-item>
+        <p2b-grid-filter-select-item label="Past 24 hours" value="24"></p2b-grid-filter-select-item>
+      </p2b-grid-filter-select>
+
+      <!-- Toggle to allow one to pause the display of incoming logs -->
+      <p2b-grid-filter-toggle key="autorefresh" label="Live Refresh" checked></p2b-grid-filter-toggle>
+
+      <!-- Columns, sorting and cell templates -->
+      <p2b-grid-column label="Level" key="level" sortable flex="2" priority="2" >
+        <template>
+          <core-icon class="level-icon {{ item.level }}" icon="{{ item.icon }}" title="{{item.level}}"></core-icon>
+          <span  moreInfoOnly style="vertical-align:middle">{{item.level}}</span>
+        </template>
+      </p2b-grid-column>
+      <p2b-grid-column label="File" key="file" sortable flex="4" minFlex="2" priority="4" >
+        <template>{{ item.file }}<span class="line-number">{{ item.fileLine }}</span></template>
+      </p2b-grid-column>
+      <p2b-grid-column label="Message" key="message" primary flex="8" minFlex="5" priority="1" >
+        <template><div class="message-text">{{ item.message }}</div></template>
+      </p2b-grid-column>
+      <p2b-grid-column label="Date" key="date" sortable flex="4" minFlex="3" priority="3">
+        <template>
+          <abbr gridOnly title="{{item.date}}">{{ item.formattedDate }}</abbr>
+          <span moreInfoOnly>{{item.date}}</span>
+        </template>
+
+      </p2b-grid-column>
+      <p2b-grid-column label="Threadid" key="threadid" sortable flex="0" priority="5">
+        <template>{{ item.threadId }}</template>
+      </p2b-grid-column>
+
+    </p2b-grid>
+  </template>
+
+  <script>
+    Polymer('p2b-plugin-vlog', {
+      refreshGrid: function() {
+        this.$.grid.refresh();
+      }
+    });
+  </script>
+</polymer-element>
diff --git a/browser/pipe-viewers/builtin/vlog/data-source.js b/browser/pipe-viewers/builtin/vlog/data-source.js
new file mode 100644
index 0000000..8876542
--- /dev/null
+++ b/browser/pipe-viewers/builtin/vlog/data-source.js
@@ -0,0 +1,87 @@
+/*
+ * Implement the data source which handles parsing the stream, searching, filtering
+ * and sorting of the vLog items.
+ * @fileoverview
+ */
+import { parse } from './parser';
+import { vLogSort } from './sorter';
+import { vLogSearch } from './searcher';
+import { vLogFilter } from './filterer';
+
+export class vLogDataSource {
+  constructor(stream, onNewItem, onError) {
+
+    /*
+     * all logs, unlimited buffer for now.
+     * @private
+     */
+    this.allLogItems = [];
+
+    /*
+     * Raw stream of log data
+     * @private
+     */
+    this.stream = stream;
+
+    stream.on('data', (line) => {
+      if (line.trim() === '') {
+        return;
+      }
+      var logItem = null;
+      // try to parse and display as much as we can.
+      try {
+        logItem = parse(line);
+      } catch (e) {
+        if (onError) {
+          onError(e);
+        }
+      }
+
+      if (logItem) {
+        this.allLogItems.push(logItem);
+        if (onNewItem) {
+          onNewItem(logItem);
+        }
+      }
+    });
+
+  }
+
+  /*
+   * Implements the fetch method expected by the grid components.
+   * handles parsing the stream, searching, filtering and sorting of the data.
+   * search, sort and filters are provided by the grid control whenever they are
+   * changed by the user.
+   * DataSource is called automatically by the grid when user interacts with the component
+   * Grid does some batching of user actions and only calls fetch when needed.
+   * keys provided for sort and filters correspond to keys set in the markup
+   * when constructing the grid.
+   * @param {object} search search{key<string>} current search keyword
+   * @param {object} sort sort{key<string>, ascending<bool>} current sort key and direction
+   * @param {map} filters map{key<string>, values<Array>} Map of filter keys to currently selected filter values
+   * @return {Array<object>} Returns an array of filtered sorted results of the items.
+   */
+  fetch(search, sort, filters) {
+
+    var filteredSortedItems = this.allLogItems.
+    filter((item) => {
+      return vLogFilter(item, filters);
+    }).
+    filter((item) => {
+      return vLogSearch(item, search.keyword);
+    }).
+    sort((item1, item2) => {
+      return vLogSort(item1, item2, sort.key, sort.ascending);
+    });
+
+    // pause or resume the stream when auto-refresh filter changes
+    if (filters['autorefresh'] && this.stream.paused) {
+      this.stream.resume();
+    }
+    if (!filters['autorefresh'] && !this.stream.paused) {
+      this.stream.pause();
+    }
+
+    return filteredSortedItems;
+  }
+}
diff --git a/browser/pipe-viewers/builtin/vlog/filterer.js b/browser/pipe-viewers/builtin/vlog/filterer.js
new file mode 100644
index 0000000..e40ebb0
--- /dev/null
+++ b/browser/pipe-viewers/builtin/vlog/filterer.js
@@ -0,0 +1,73 @@
+/*
+ * Returns whether the given vLogItem matches the map of filters.
+ * @param {Object} item A single veyron log item as defined by parser.item
+ * @param {map} filters Map of keys to selected filter values as defined
+ * when constructing the filters in the grid components.
+ * e.g. filters:{'date':'all', 'levels':['info','warning']}
+ * @return {boolean} Whether the item satisfies ALL of the given filters.
+ */
+export function vLogFilter(item, filters) {
+  if (Object.keys(filters).length === 0) {
+    return true;
+  }
+
+  for (var key in filters) {
+    var isMatch = applyFilter(item, key, filters[key]);
+    // we AND all the filters, short-circuit for early termination
+    if (!isMatch) {
+      return false;
+    }
+  }
+
+  // matches all filters
+  return true;
+};
+
+/*
+ * Returns whether the given vLogItem matches a single filter
+ * @param {Object} item A single veyron log item as defined by parser.item
+ * @param {string} key filter key e.g. 'date'
+ * @param {string} value filter value e.g. 'all'
+ * @return {boolean} Whether the item satisfies then the given filter key value pair
+ * @private
+ */
+function applyFilter(item, key, value) {
+  switch (key) {
+    case 'date':
+      return filterDate(item, value);
+    case 'level':
+      return filterLevel(item, value);
+    default:
+      // ignore unknown filters
+      return true;
+  }
+}
+
+/*
+ * Returns whether item's date satisfies the date filter
+ * @param {Object} item A single veyron log item as defined by parser.item
+ * @param {string} since One of 'all', '1' or '24'. for Anytime, past one hour, past 24 hours.
+ * @return {boolean} whether item's date satisfies the date filter
+ * @private
+ */
+function filterDate(item, since) {
+  if (since === 'all') {
+    return true;
+  } else {
+    var hours = parseInt(since);
+    var targetDate = new Date();
+    targetDate.setHours(targetDate.getHours() - hours);
+    return item.date > targetDate;
+  }
+}
+
+/*
+ * Returns whether item's level is one of the given values
+ * @param {Object} item A single veyron log item as defined by parser.item
+ * @param {Array<string>} levels Array of level values e.g. ['info','warning']
+ * @return {boolean} whether item's level is one of the given values
+ * @private
+ */
+function filterLevel(item, levels) {
+  return levels.indexOf(item.level) >= 0;
+}
diff --git a/browser/pipe-viewers/builtin/vlog/parser.js b/browser/pipe-viewers/builtin/vlog/parser.js
new file mode 100644
index 0000000..f208595
--- /dev/null
+++ b/browser/pipe-viewers/builtin/vlog/parser.js
@@ -0,0 +1,89 @@
+/*
+ * Parse utilities for veyron logs
+ * @fileoverview
+ */
+
+/*
+ * Parses a single line of text produced by veyron logger
+ * into an structured object representing it.
+ * Log lines have this form:
+ * Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg...
+ * where the fields are defined as follows:
+ *  L                A single character, representing the log level (eg 'I' for INFO)
+ *  mm               The month (zero padded; ie May is '05')
+ *  dd               The day (zero padded)
+ *  hh:mm:ss.uuuuuu  Time in hours, minutes and fractional seconds
+ *  threadid         The space-padded thread ID as returned by GetTID()
+ *  file             The file name
+ *  line             The line number
+ *  msg              The user-supplied message
+ * @param {string} vlogLine A single line of veyron log
+ * @return {parser.item} A parsed object containing log level, date, file,
+ * line number, thread id and message.
+ */
+export function parse(vlogLine) {
+
+  var validLogLineRegEx = /^([IWEF])(\d{2})(\d{2})\s(\d{2}:\d{2}:\d{2}\.\d+)\s(\d+)\s(.*):(\d+)]\s+(.*)$/
+  var logParts = vlogLine.match(validLogLineRegEx);
+  if (!logParts || logParts.length != 8 + 1) { // 8 parts + 1 whole match
+    throw new Error('Invalid vlog line format. ' + vlogLine +
+      ' Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg.. pattern');
+  }
+
+  var L = logParts[1];
+  var month = logParts[2];
+  var day = logParts[3];
+  var time = logParts[4];
+  var treadId = parseInt(logParts[5]);
+  var file = logParts[6];
+  var fileLine = parseInt(logParts[7]);
+  var message = logParts[8];
+
+  var now = new Date();
+  var year = now.getFullYear();
+  var thisMonth = now.getMonth() + 1; // JS months are 0-11
+  // Year flip edge case, if log month > this month, we assume log line is from previous year
+  if (parseInt(month) > thisMonth) {
+    year--;
+  }
+
+  var date = new Date(year + '-' + month + '-' + day + ' ' + time);
+
+  return new item(
+    levelCodes[L],
+    date,
+    treadId,
+    file,
+    fileLine,
+    message
+  );
+}
+
+var levelCodes = {
+  'I': 'info',
+  'W': 'warning',
+  'E': 'error',
+  'F': 'fatal'
+}
+
+/*
+ * A structure representing a veyron log item
+ * @param {string} level, one of info, warning, error, fatal
+ * @param {date} date, The date and time of the log item
+ * @param {integer} threadId The thread ID as returned by GetTID()
+ * @param {string} file The file name
+ * @param {integer} fileLine The file line number
+ * @param {string} message The user-supplied message
+ * @class
+ * @private
+ */
+class item {
+  constructor(level, date, threadId, file, fileLine, message) {
+    this.level = level;
+    this.date = date;
+    this.threadId = threadId;
+    this.file = file;
+    this.fileLine = fileLine;
+    this.message = message;
+  }
+}
diff --git a/browser/pipe-viewers/builtin/vlog/plugin.js b/browser/pipe-viewers/builtin/vlog/plugin.js
new file mode 100644
index 0000000..589dcfa
--- /dev/null
+++ b/browser/pipe-viewers/builtin/vlog/plugin.js
@@ -0,0 +1,100 @@
+/*
+ * vlog is a Pipe Viewer that displays veyron logs in a graphical grid.
+ * Please note that Veyron writes logs to stderr stream, in *nix systems 2>&1
+ * can be used to redirect stderr to stdout which can be then piped to P2B.
+ * @tutorial myVeyronServerd -v=3 2>&1 | p2b google/p2b/[name]/vlog
+ * @tutorial cat logfile.txt | p2b google/p2b/[name]/vlog
+ * @fileoverview
+ */
+import { View } from 'view';
+import { PipeViewer } from 'pipe-viewer';
+import { streamUtil } from 'stream-helpers';
+import { formatDate } from 'formatting';
+import { Logger } from 'logger'
+import { vLogDataSource } from './data-source';
+
+var log = new Logger('pipe-viewers/builtin/vlog');
+
+class vLogPipeViewer extends PipeViewer {
+  get name() {
+    return 'vlog';
+  }
+
+  play(stream) {
+    stream.setEncoding('utf8');
+
+    var logView = document.createElement('p2b-plugin-vlog');
+    var newData = true;
+    var refreshGrid = function() {
+      requestAnimationFrame(() => {
+        if( newData ) {
+          logView.refreshGrid();
+          newData = false;
+        }
+        refreshGrid();
+      });
+    };
+    refreshGrid();
+
+    // split by new line
+    stream = stream.pipe(streamUtil.split(/\r?\n/));
+
+    // create a new data source from the stream and set it.
+    logView.dataSource = new vLogDataSource(
+      stream,
+      function onNewItem(item) {
+        newData = true;
+        // add additional, UI related properties to the item
+        addAdditionalUIProperties(item);
+      },
+      function onError(err) {
+        log.debug(err);
+      });
+
+    return new View(logView);
+  }
+}
+
+/*
+ * Adds additional UI specific properties to the item
+ * @private
+ */
+function addAdditionalUIProperties(item) {
+  addIconProperty(item);
+  addFormattedDate(item);
+}
+
+/*
+ * Adds an icon property to the item specifying what icon to display
+ * based on log level
+ * @private
+ */
+function addIconProperty(item) {
+  var iconName = 'info';
+  switch (item.level) {
+    case 'info':
+      iconName = 'info-outline';
+      break;
+    case 'warning':
+      iconName = 'warning';
+      break;
+    case 'error':
+      iconName = 'info';
+      break;
+    case 'fatal':
+      iconName = 'block';
+      break;
+  }
+
+  item.icon = iconName;
+}
+
+/*
+ * Adds a human friendly date field
+ * @private
+ */
+function addFormattedDate(item) {
+  item.formattedDate = formatDate(item.date);
+}
+
+export default vLogPipeViewer;
diff --git a/browser/pipe-viewers/builtin/vlog/searcher.js b/browser/pipe-viewers/builtin/vlog/searcher.js
new file mode 100644
index 0000000..46bcfbf
--- /dev/null
+++ b/browser/pipe-viewers/builtin/vlog/searcher.js
@@ -0,0 +1,14 @@
+export function vLogSearch(logItem, keyword) {
+  if (!keyword) {
+    return true;
+  }
+
+  // we do a contains for message, file and threadId fields only
+  if (logItem.message.indexOf(keyword) >= 0 ||
+    logItem.file.indexOf(keyword) >= 0 ||
+    logItem.threadId.toString().indexOf(keyword) >= 0) {
+    return true
+  }
+
+  return false;
+};
diff --git a/browser/pipe-viewers/builtin/vlog/sorter.js b/browser/pipe-viewers/builtin/vlog/sorter.js
new file mode 100644
index 0000000..664a3fa
--- /dev/null
+++ b/browser/pipe-viewers/builtin/vlog/sorter.js
@@ -0,0 +1,26 @@
+var levelSortPriority = {
+  'fatal' : 1,
+  'error' : 2,
+  'warning' : 3,
+  'info': 4
+}
+
+export function vLogSort(item1, item2, key, ascending) {
+  var first = item1[key];
+  var second = item2[key];
+  if (!ascending) {
+    first = item2[key];
+    second = item1[key];
+  }
+
+  if (key === 'level') {
+    first = levelSortPriority[first];
+    second = levelSortPriority[second];
+  }
+
+  if (typeof first === 'string') {
+    return first.localeCompare(second);
+  } else {
+    return first - second;
+  }
+};
diff --git a/browser/pipe-viewers/manager.js b/browser/pipe-viewers/manager.js
new file mode 100644
index 0000000..b088835
--- /dev/null
+++ b/browser/pipe-viewers/manager.js
@@ -0,0 +1,91 @@
+/*
+ * Pipe viewer manager is used to load and get an instance of a pipe viewer
+ * given its name.
+ *
+ * Manager handles on-demand loading and caching of pipe viewers.
+ * @fileoverview
+ */
+
+import { isAbsoulteUrl } from 'libs/utils/url'
+import { Logger } from 'libs/logs/logger'
+
+/*
+ * Preload certain common builtin plugins.
+ * Plugins are normally loaded on demand and this makes the initial bundle larger
+ * but common plugins should be preloaded for better performance.
+ * This is kind of a hack as it simply exposes a path to these
+ * plugins so that build bundler finds them and bundles them with the reset of the app
+ */
+import { default as vlogPlugin } from './builtin/vlog/plugin'
+import { default as imagePlugin } from './builtin/image/plugin'
+import { default as consolePlugin } from './builtin/console/plugin'
+
+var log = new Logger('pipe-viewer/manager');
+
+// cache loaded viewers
+var loadedPipeViewers = {};
+
+/*
+ * Asynchronously loads and returns a PipeViewer plugin instance given its name.
+ * @param {string} name Unique name of the viewer.
+ * @return {Promise<PipeViewer>} pipe viewer for the given name
+ */
+export function get(name) {
+  if(isLoaded(name)) {
+    return Promise.resolve(new loadedPipeViewers[name]());
+  }
+
+  return loadViewer(name).then((viewerClass) => {
+    return new viewerClass();
+  }).catch((e) => { return Promise.reject(e); });
+}
+
+/*
+ * Tests whether the viewer plugin is already loaded or not.
+ * @param {string} name Unique name of the viewer.
+ * @return {string} Whether the viewer plugin is already loaded or not.
+ *
+ * @private
+ */
+function isLoaded(name) {
+  return loadedPipeViewers[name] !== undefined
+}
+
+/*
+ * Registers a pipeViewer under a unique name and make it available to be called
+ * @param {string} name Unique name of the viewer.
+ * @return {Promise} when import completes.
+ *
+ * @private
+ */
+function loadViewer(name) {
+  log.debug('loading viewer:', name);
+
+  var path = getPath(name);
+  return System.import(path).then((module) => {
+    var pipeViewerClass = module.default;
+    loadedPipeViewers[name] = pipeViewerClass;
+    return pipeViewerClass;
+  }).catch((e) => {
+    var errMessage = 'could not load viewer for: ' + name;
+    log.debug(errMessage, e);
+    return Promise.reject(new Error(errMessage));
+  })
+}
+
+/*
+ * Returns the path to a pipe viewer module location based on its name.
+ * @param {string} name Unique name of the viewer.
+ * @return {string} path to a pipe viewer module location.
+ *
+ * @private
+ */
+function getPath(name) {
+  if(isAbsoulteUrl(name)) {
+    var encodedName = encodeURIComponent(name);
+    System.paths[encodedName] = name;
+    return encodedName;
+  } else {
+    return 'pipe-viewers/builtin/' + name + '/plugin';
+  }
+}
diff --git a/browser/pipe-viewers/pipe-viewer-delegation.js b/browser/pipe-viewers/pipe-viewer-delegation.js
new file mode 100644
index 0000000..f1e1adf
--- /dev/null
+++ b/browser/pipe-viewers/pipe-viewer-delegation.js
@@ -0,0 +1,17 @@
+import { get as getPipeViewer } from 'pipe-viewers/manager'
+
+/*
+ * Allows a pipe-viewer plugin to delegate playing of a stream to another
+ * pipe-viewer plugin.
+ * Useful for cases where a plugin wants to simply do some transforms on the stream
+ * and then have it be played by an existing plugin.
+ * @param {string} pipeViewerName of the pipe-viewer to redirect to.
+ * @param {Veyron.Stream} stream Stream of data to be redirected.
+ * @return {Promise<View>} A promise of an View from the target pipe viewer, which can
+ * be returned from the play() method of the caller plugin.
+ */
+export function redirectPlay(pipeViewerName, stream) {
+  return getPipeViewer(pipeViewerName).then((pipeViewer) => {
+    return pipeViewer.play(stream);
+  });
+}
diff --git a/browser/pipe-viewers/pipe-viewer.js b/browser/pipe-viewers/pipe-viewer.js
new file mode 100644
index 0000000..738fb01
--- /dev/null
+++ b/browser/pipe-viewers/pipe-viewer.js
@@ -0,0 +1,32 @@
+/*
+ * Defines the interface expected to be implemented by any pipe viewer plugin.
+ * A PipeViewer is an object that can render a stream of data.
+ *
+ * It has a unique name which is used to find the viewer plugin and direct
+ * requests to it.
+ *
+ * It has a play function that will be called and supplied by a stream object.
+ * play(stream) needs to return a View or promise of a View
+ * that will be appended to the UI by the p2b framework.
+ * If a promise, p2b UI will display a loading widget until promise is resolved.
+ */
+export class PipeViewer {
+
+  /*
+   * @property {string} name Unique name of the viewer.
+   */
+  get name() {
+    throw new Error('Abstract method. Must be implemented by subclasses');
+  }
+
+  /*
+   * play() function is called by the p2b framework when a pipe request for the
+   * this specific pipe viewer comes in.
+   * @param {Veyron.Stream} stream Stream of data to be displayed.
+   * @return {Promise<View>|{View}} a View or a promise of an
+   * View that p2b can display.
+   */
+  play(stream) {
+    throw new Error('Abstract method. Must be implemented by subclasses');
+  }
+}
diff --git a/browser/runtime/context.js b/browser/runtime/context.js
new file mode 100644
index 0000000..a8fc1e0
--- /dev/null
+++ b/browser/runtime/context.js
@@ -0,0 +1,21 @@
+/*
+ * Holds references to runtime context but instead of one big context object
+ * different context are exposed as their own object allowing other modules
+ * just pick the context they need.
+ * @fileoverview
+ */
+
+import { PageView } from 'views/page/view'
+import { PipesView } from 'views/pipes/view'
+
+/*
+ * Reference to the current page view object constructed by the application
+ * @type {View}
+ */
+export var page = new PageView();
+
+/*
+ * Reference to the current pipes view object constructed by the application
+ * @type {View}
+ */
+export var pipesViewInstance = new PipesView();
diff --git a/browser/services/pipe-to-browser-client.js b/browser/services/pipe-to-browser-client.js
new file mode 100644
index 0000000..f2c43c7
--- /dev/null
+++ b/browser/services/pipe-to-browser-client.js
@@ -0,0 +1,28 @@
+/*
+ * Implements a veyron client that can talk to a P2B service.
+ * @fileoverview
+ */
+import { Logger } from 'libs/logs/logger'
+import veyron from 'veyronjs'
+
+var log = new Logger('services/p2b-client');
+
+/*
+ * Pipes a stream of data to the P2B service identified
+ * by the given veyron name.
+ * @param {string} name Veyron name of the destination service
+ * @param {Stream} Stream of data to pipe to it.
+ * @return {Promise} Promise indicating if piping was successful or not
+ */
+export function pipe(name, stream) {
+  return veyron.init().then((runtime) => {
+    var client = runtime.newClient();
+    var ctx = runtime.getContext().withTimeout(5000);
+    ctx.waitUntilDone(function(){});
+    return client.bindTo(ctx, name).then((remote) => {
+      var remoteStream = remote.pipe(ctx).stream;
+      stream.pipe(remoteStream);
+      return Promise.resolve();
+    });
+  });
+}
diff --git a/browser/services/pipe-to-browser-namespace.js b/browser/services/pipe-to-browser-namespace.js
new file mode 100644
index 0000000..df1a7c0
--- /dev/null
+++ b/browser/services/pipe-to-browser-namespace.js
@@ -0,0 +1,34 @@
+/*
+ * Implements a veyron client that talks to the namespace service and finds all
+ * the P2B services that are available.
+ * @fileoverview
+ */
+import { Logger } from 'libs/logs/logger'
+import veyron from 'veyronjs'
+
+var log = new Logger('services/p2b-namespace');
+
+/*
+ * Finds all the P2B services that are published by querying the namespace.
+ * @return {Promise} Promise resolving to an array of names for all published
+ * P2B services
+ */
+export function getAll() {
+  return veyron.init().then((runtime) => {
+    var namespace = runtime.namespace();
+    var ctx = runtime.getContext().withTimeout(5000);
+    ctx.waitUntilDone(function(){});
+
+    var globResult = namespace.glob(ctx, 'google/p2b/*');
+    var p2bServices = [];
+    globResult.stream.on('data', (p2bServiceName) => {
+      p2bServices.push(p2bServiceName.name);
+    });
+
+    // wait until all the data arrives then return the collection
+    return globResult.then(() => {
+      ctx.cancel();
+      return p2bServices;
+    });
+  });
+}
diff --git a/browser/services/pipe-to-browser-server.js b/browser/services/pipe-to-browser-server.js
new file mode 100644
index 0000000..6229d37
--- /dev/null
+++ b/browser/services/pipe-to-browser-server.js
@@ -0,0 +1,135 @@
+/*
+ * Implements and publishes a Veyron service which accepts streaming RPC
+ * requests and delegates the stream back to the provided pipeRequestHandler.
+ * It also exposes the state of the service.
+ * @fileoverview
+ */
+import { Logger } from 'libs/logs/logger'
+import { ByteObjectStreamAdapter } from 'libs/utils/byte-object-stream-adapter'
+import { StreamByteCounter } from 'libs/utils/stream-byte-counter'
+import { StreamCopy } from 'libs/utils/stream-copy'
+import veyron from 'veyronjs'
+
+var log = new Logger('services/p2b-server');
+var server;
+
+// State of p2b service
+export var state = {
+  init() {
+    this.published = false;
+    this.publishing = false;
+    this.stopping = false;
+    this.fullServiceName = null;
+    this.date = null;
+    this.numPipes = 0;
+    this.numBytes = 0;
+  },
+  reset() {
+    state.init();
+  }
+};
+state.init();
+
+/*
+ * Publishes the p2b service under google/p2b/{name}
+ * e.g. If name is "john-tablet", p2b service will be accessible under name:
+ * 'google/p2b/john-tablet'
+ *
+ * pipe() method can be invoked on any 'google/p2b/{name}/suffix' name where
+ * suffix identifies the viewer that can format and display the stream data
+ * e.g. 'google/p2b/john-tablet/console'.pipe() will display the incoming
+ * data in a data table. See /app/viewer/ for a list of available viewers.
+ * @param {string} name Name to publish the service under
+ * @param {function} pipeRequestHandler A function that will be called when
+ * a request to handle a pipe stream comes in.
+ * @return {Promise} Promise that will resolve or reject when publish completes
+ */
+export function publish(name, pipeRequestHandler) {
+  log.debug('publishing under name:', name);
+  /*
+   * Veyron pipe to browser service implementation.
+   * Implements the p2b VDL.
+   */
+  var p2b = {
+    pipe(ctx, $stream) {
+      return new Promise(function(resolve, reject) {
+        log.debug('received pipe request for:', ctx.suffix);
+        var numBytesForThisCall = 0;
+
+        var bufferStream = new ByteObjectStreamAdapter();
+        var streamByteCounter = new StreamByteCounter((numBytesRead) => {
+          // increment total number of bytes received and total for this call
+          numBytesForThisCall += numBytesRead;
+          state.numBytes += numBytesRead;
+        });
+
+        var streamCopier = $stream.pipe(new StreamCopy());
+        var stream = streamCopier.pipe(bufferStream).pipe(streamByteCounter);
+        stream.copier = streamCopier;
+
+        streamByteCounter.on('end', () => {
+          log.debug('end of stream');
+          // send total number of bytes received for this call as final result
+          resolve();
+        });
+
+        stream.on('error', (e) => {
+          log.debug('stream error', e);
+          // TODO(aghassemi) envyor issue #50
+          // we want to reject but because of #50 we can't
+          // reject('Browser P2B threw an exception. Please see browser console for details.');
+          // reject(e);
+          resolve();
+        });
+
+        state.numPipes++;
+        try {
+          pipeRequestHandler(ctx.suffix, stream);
+        } catch(e) {
+          // TODO(aghassemi) envyor issue #50
+          // we want to reject but because of #50 we can't
+          // reject('Browser P2B threw an exception. Please see browser console for details.');
+          log.debug('pipeRequestHandler error', e);
+          resolve();
+        }
+      });
+    }
+  };
+
+  state.publishing = true;
+
+  return veyron.init().then((runtime) => {
+    server = runtime.newServer();
+    var serviceName = 'google/p2b/' + name;
+
+    // TODO(nlacasee,sadovsky): Our current authorization policy never returns
+    // any errors, i.e. everyone is authorized!
+    var openAuthorizer = function(){ return null; };
+    var options = {authorizer: openAuthorizer};
+
+    return server.serve(serviceName, p2b, options).then(() => {
+      log.debug('published!');
+
+      state.published = true;
+      state.publishing = false;
+      state.fullServiceName = serviceName;
+      state.date = new Date();
+
+      return;
+    });
+  }).catch((err) => { state.reset(); throw err; });
+}
+
+/*
+ * Stops the service and unpublishes it, effectively destroying the service.
+ * @return {Promise} Promise that will resolve or reject when stopping completes
+ */
+export function stopPublishing() {
+  log.debug('stopping service');
+  state.stopping = true;
+  return server.stop().then(function() {
+    log.debug('service stopped');
+    state.reset();
+    return;
+  });
+}
diff --git a/browser/views/common/common.css b/browser/views/common/common.css
new file mode 100644
index 0000000..a114380
--- /dev/null
+++ b/browser/views/common/common.css
@@ -0,0 +1,31 @@
+.hidden {
+  display: none !important;
+}
+
+paper-button {
+  margin: 1em;
+  width: 10em;
+  display: block;
+  cursor: default;
+}
+
+paper-button.colored {
+  background: #4285f4;
+  color:#FFFFFF;
+}
+
+paper-button.colored.red {
+  background: #d34336;
+}
+
+[flex] {
+  display: flex;
+  flex-direction: column;
+  flex: 1 1 0px;
+}
+
+[page-title] {
+  font-size: 1.5em;
+  color: #4285f4;
+  margin: 0px;
+}
diff --git a/browser/views/error/broken_robot.png b/browser/views/error/broken_robot.png
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/browser/views/error/broken_robot.png
diff --git a/browser/views/error/component.css b/browser/views/error/component.css
new file mode 100644
index 0000000..78e6bb8
--- /dev/null
+++ b/browser/views/error/component.css
@@ -0,0 +1,19 @@
+:host {
+  background-image: url('broken_robot.png');
+  background-repeat: no-repeat;
+  background-position: right 50%;
+  min-height: 300px;
+  width: 100%;
+  max-width: 600px;
+  display: block;
+}
+
+h2 {
+  font-size: 1.1em;
+}
+
+h3 {
+  font-size: 0.9em;
+  color: rgba(0,0,0,0.54);
+  margin-right: 100px;
+}
diff --git a/browser/views/error/component.html b/browser/views/error/component.html
new file mode 100644
index 0000000..93782e7
--- /dev/null
+++ b/browser/views/error/component.html
@@ -0,0 +1,15 @@
+<link rel="import" href="../../third-party/polymer/polymer.html">
+
+<polymer-element name="p2b-error">
+  <template>
+    <link rel="stylesheet" href="component.css">
+    <h2 page-title>Error</h2>
+    <h3>Sorry, some of the 1s and 0s got mixed.</h3>
+    <h3>{{errorMessage}}</h3>
+  </template>
+  <script>
+    Polymer('p2b-error', {
+      errorMessage: null
+    });
+    </script>
+</polymer-element>
diff --git a/browser/views/error/view.js b/browser/views/error/view.js
new file mode 100644
index 0000000..bfbc5b1
--- /dev/null
+++ b/browser/views/error/view.js
@@ -0,0 +1,38 @@
+import { exists } from 'libs/utils/exists'
+import { View } from 'libs/mvc/view'
+import { Logger } from 'libs/logs/logger'
+
+var log = new Logger('views/error');
+
+/*
+ * View representing application error.
+ * @param {Error|String} err Error to display
+ * @class
+ * @extends {View}
+ */
+export class ErrorView extends View {
+	constructor(err) {
+		var el = document.createElement('p2b-error');
+		super(el);
+
+		this.error = err;
+	}
+
+	set error(err) {
+		if(!exists(err)) {
+			return;
+		}
+
+		var errorMessage = err.toString();
+		log.debug(errorMessage);
+		if(exists(err.stack)) {
+			log.debug(err.stack);
+		}
+
+		this.element.errorMessage = errorMessage;
+	}
+
+	get error() {
+		return this.element.errorMessage;
+	}
+}
diff --git a/browser/views/help/component.css b/browser/views/help/component.css
new file mode 100644
index 0000000..a1ce652
--- /dev/null
+++ b/browser/views/help/component.css
@@ -0,0 +1,33 @@
+.name {
+  font-weight: bold;
+  color: rgba(0, 0, 0, 0.8);
+}
+
+.mono, .code {
+  font-size: 1.1em;
+  font-family: monospace;
+}
+
+.code {
+  background-color: #222;
+  color: #fafafa;
+  padding: 1em;
+  white-space: normal;
+  word-break: break-all;
+}
+
+h3 {
+  font-size: 1.3em;
+}
+
+h4 {
+  font-size: 1.2em;
+  color: rgb(51, 103, 214);
+  margin-bottom: 0.5em;
+}
+
+a {
+  color: #5677fc;
+  text-decoration: none;
+  cursor: pointer;
+}
diff --git a/browser/views/help/component.html b/browser/views/help/component.html
new file mode 100644
index 0000000..d3861ef
--- /dev/null
+++ b/browser/views/help/component.html
@@ -0,0 +1,71 @@
+<link rel="import" href="../../third-party/polymer/polymer.html">
+
+<polymer-element name="p2b-help">
+<template>
+  <link rel="stylesheet" href="component.css">
+  <link rel="stylesheet" href="../common/common.css">
+  <h2 page-title>Help</h2>
+  <p>Pipe To Browser allows you to pipe anything from shell console to the browser. Piped data is then displayed in a graphical and formatted way by a viewer you can specify.</p>
+  <h3>Getting Started</h3>
+  <template if="{{serviceState.published}}">
+    <p>Looks like you have already started the service under <span class="name">{{publishedName}}</span>, great!</p>
+  </template>
+  <template if="{{!serviceState.published}}">
+    <p>Before we start, you need to start the service under a name. Go to Home and publish this instance of P2B under a name like <span class="name">john-tablet</span> or <span class="name">jane-desktop</span>
+    </p>
+  </template>
+  <p>Now let's use the <span class="name">console</span> viewer. It can pretty much display anything, so it's a good one to start with</p>
+  <p>In your Linux or Mac console run:</p>
+  <pre class="code">echo "Hello World" | p2b {{publishedName}}/console</pre>
+  <p>P2B follows a basic <span class="mono">cmd | p2b google/p2b/[name]/[viewer]</span> pattern. Where <span class="mono">[name]</span> is what you publish the service under and <span class="mono">[viewer]</span> can be the name of a built-in viewer like <span class="mono">image</span> or <span class="mono">console</span> or a Url to a remote viewer that is a P2B plug-in.</p>
+  <h3>Built-in Viewers</h3>
+  <p>In addition to the basic <span class="name">console</span> viewer, P2B is preloaded with the following viewers</p>
+
+  <h4>Image</h4>
+  <p><span class="name">image</span> can display most types of images.</p>
+  <pre class="code">cat grumpy-cat.jpg | p2b {{publishedName}}/image</pre>
+
+  <h4>Git Status</h4>
+  <p>Ever wanted to sort, search and filter result of <span class="mono">git status</span> to make sense of it all? <span class="name">git/status</span> can do that. You need to use <span class="mono">git status --short</span> though, so we can parse it.</p>
+  <pre class="code">git status --short | p2b {{publishedName}}/git/status</pre>
+
+  <h4>Veyron Log Viewer</h4>
+  <span class="name">vlog</span> displays Veyron logs in a DataGrid and supports sorting, searching, paging, pausing and filtering based on time and log level. DataGrid is responsive and may hide columns on smaller screens but you can always see all the fields by using the more info icon.</p>
+  <pre class="code">cat vlogfile.txt | p2b {{publishedName}}/vlog</pre>
+  <p>If you want to pipe logs from a Veyron service directly, you need to pipe stderr or strout first using <span class="mono">2&gt;&amp;1</span></p>
+  <pre class="code">myVeyronServerd -v=3 2&gt;&amp;1 | p2b {{publishedName}}/vlog</pre>
+
+  <h4>Panic Console</h4>
+  <span class="name">console/panic</span> renders plaintext as <span class="name">console</span> until it detects a Go panic. Go panics crash every running goroutine, leading to a spew of stderr logs. This plugin groups goroutine crash logs into show/hide blocks. The plugin stops scrolling at the first goroutine block, which caused the panic. To further assist the debugging process, lines can be highlighted by keyword using an input filter. Don't forget to pipe both stdout and stderr to this plugin!
+  <pre class="code">./goProgram 2>&1 | p2b {{publishedName}}/console/panic</pre>
+
+  <h4>dev/null</h4>
+  <p>No system is complete without a <span class="name">dev/null</span>. Similar to *nix <span class="mono">dev/null</span>, anything piped to it will be discarded without mercy.</p>
+  <pre class="code">cat /dev/urandom | p2b {{publishedName}}/dev/null</pre>
+
+  <h3>Remote Viewers</h3>
+  <p>In addition to built-in viewers, ad-hoc remote viewers can be hosted anywhere and used with P2B. Remote viewers are referenced by the Url of the plug-in JavaScript file</p>
+  <pre class="code">echo "Hello World" | p2b {{publishedName}}/http://googledrive.com/host/0BzmT5cnKdCAKa3hzNEVCU2tnd3c/helloworld.js</pre>
+  <p>Writing remote viewers is not different than writing built-in ones and basic plug-ins are pretty straight forward to write.</p>
+  <p>At high level, plug-ins are expected to implement a <span class="mono">PipeViewer</span> interface which has a <span class="mono">play(stream)</span> method. A <span class="mono">view</span> (which is a wrapper for a DOM element) is expected to be returned from <span class="mono">play(stream)</span>. You can look at the hello world remote plug-in <a href="http://googledrive.com/host/0BzmT5cnKdCAKa3hzNEVCU2tnd3c/helloworld.js" target="_blank">code on Google drive</a> to get started on writing new remote plug-ins</p>
+  <p>It is also possible to write the UI layer of your plug-in in HTML and CSS as a Web Component to avoid mixing logic and layout/styling in a single file.</p>
+  <p>Grumpy cat meme plug-in takes that approach. You can look at the <a href="http://googledrive.com/host/0BzmT5cnKdCAKV1p6Q0pjak5Kams/meme.js" target="_blank">JavaScript</a> and <a onClick="window.open('view-source:' + 'http://googledrive.com/host/0BzmT5cnKdCAKV1p6Q0pjak5Kams/meme.html');">HTML Web Component</a> source files.</p>
+  <pre class="code">echo "I take stuff from stdin, and send them to /dev/null" | p2b {{publishedName}}/http://googledrive.com/host/0BzmT5cnKdCAKV1p6Q0pjak5Kams/meme.js</pre>
+</template>
+<script>
+  Polymer('p2b-help', {
+      /*
+       * Dynamic binding for the state of publishing p2b service.
+       */
+       serviceState: null,
+       get publishedName() {
+        if( this.serviceState && this.serviceState.published ) {
+          return this.serviceState.fullServiceName
+        } else {
+          return 'google/p2b/[name]';
+        }
+      },
+
+    });
+  </script>
+</polymer-element>
diff --git a/browser/views/help/view.js b/browser/views/help/view.js
new file mode 100644
index 0000000..5357a13
--- /dev/null
+++ b/browser/views/help/view.js
@@ -0,0 +1,15 @@
+import { exists } from 'libs/utils/exists'
+import { View } from 'libs/mvc/view'
+
+/*
+ * View representing the help page
+ * @class
+ * @extends {View}
+ */
+export class HelpView extends View {
+	constructor(serviceState) {
+		var el = document.createElement('p2b-help');
+    el.serviceState = serviceState;
+		super(el);
+	}
+}
diff --git a/browser/views/loading/component.css b/browser/views/loading/component.css
new file mode 100644
index 0000000..a986c5c
--- /dev/null
+++ b/browser/views/loading/component.css
@@ -0,0 +1,3 @@
+.spinner {
+  margin: 1em;
+}
diff --git a/browser/views/loading/component.html b/browser/views/loading/component.html
new file mode 100644
index 0000000..c4ad3ef
--- /dev/null
+++ b/browser/views/loading/component.html
@@ -0,0 +1,12 @@
+<link rel="import" href="../../third-party/polymer/polymer.html">
+
+<polymer-element name="p2b-loading">
+  <template>
+    <link rel="stylesheet" href="component.css">
+    <img class="spinner" src="../../libs/ui-components/common/spinner.gif" alt="Loading"/>
+  </template>
+  <script>
+    Polymer('p2b-loading', {
+    });
+  </script>
+</polymer-element>
diff --git a/browser/views/loading/view.js b/browser/views/loading/view.js
new file mode 100644
index 0000000..52bf9fc
--- /dev/null
+++ b/browser/views/loading/view.js
@@ -0,0 +1,13 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View representing a loading indicator
+ * @class
+ * @extends {View}
+ */
+export class LoadingView extends View {
+  constructor() {
+    var el = document.createElement('p2b-loading');
+    super(el);
+  }
+}
diff --git a/browser/views/namespace-list/component.css b/browser/views/namespace-list/component.css
new file mode 100644
index 0000000..93ea771
--- /dev/null
+++ b/browser/views/namespace-list/component.css
@@ -0,0 +1,19 @@
+[selectable] .selected {
+  background-color: #00e5ff;
+  opacity: 0.8;
+}
+
+[selectable] core-item {
+  cursor: pointer;
+}
+
+core-item {
+  box-sizing: border-box;
+  border-bottom: 1px solid rgba(0,0,0,0.05);
+  margin: 0;
+  padding: 0 1em;
+}
+
+core-item:last-child {
+  border-bottom: none;
+}
diff --git a/browser/views/namespace-list/component.html b/browser/views/namespace-list/component.html
new file mode 100644
index 0000000..1f4b89b
--- /dev/null
+++ b/browser/views/namespace-list/component.html
@@ -0,0 +1,53 @@
+<link rel="import" href="../../third-party/polymer/polymer.html">
+<link rel="import" href="../../third-party/core-list/core-list.html">
+<link rel="import" href="../../third-party/core-item/core-item.html">
+
+<polymer-element name="p2b-namespace-list" attributes="names selectable">
+  <template>
+    <link rel="stylesheet" href="component.css">
+    <core-list selectable?="{{selectable}}" on-core-activate="{{fireSelectEvent}}" data="{{_items}}" height="20" style="height: 200px">
+      <template>
+        <core-item class="{{ {selected: selected} | tokenList }}" label="{{model.name}}"></core-item>
+      </template>
+    </core-list>
+  </template>
+  <script>
+    Polymer('p2b-namespace-list', {
+     /*
+      * List of names to be displayed
+      * @type {Array<string>}
+      */
+      names: [],
+
+     /*
+      * Whether the names displayed are selectable
+      * if selectable, 'select' event will fire with the name of the selected item
+      * @type {boolean}
+      */
+      selectable: false,
+
+      /*
+       * transformed collection of names to objects
+       * @private
+       */
+      _items: [],
+      namesChanged: function() {
+        // transform from [string] to [object] since core-items expects array of objects
+        this._items = this.names.map( function(n) {
+          return {name: n};
+        });
+      },
+
+      /*
+       * fires the select event pass the name as event argument
+       * @private
+       */
+      fireSelectEvent: function(e) {
+        if (!this.selectable) {
+          return;
+        }
+        this.fire('select', e.detail.data.name);
+      }
+    });
+    </script>
+</polymer-element>
diff --git a/browser/views/namespace-list/view.js b/browser/views/namespace-list/view.js
new file mode 100644
index 0000000..3747d81
--- /dev/null
+++ b/browser/views/namespace-list/view.js
@@ -0,0 +1,25 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View showing a list of all p2b services from the namespace
+ * @class
+ * @extends {View}
+ */
+export class NamespaceListView extends View {
+	constructor(items) {
+		var el = document.createElement('p2b-namespace-list');
+		el.items = items;
+		super(el);
+	}
+
+/*
+ * Event that fires when user selects an item from the list.
+ * @event
+ * @type {string} name of the item that was selected
+ */
+  onSelectAction(eventHandler) {
+    this.element.addEventListener('select', (e) => {
+      eventHandler(e.detail);
+    });
+  }
+}
diff --git a/browser/views/neighborhood/component.html b/browser/views/neighborhood/component.html
new file mode 100644
index 0000000..6b1c154
--- /dev/null
+++ b/browser/views/neighborhood/component.html
@@ -0,0 +1,20 @@
+<link rel="import" href="../../third-party/polymer/polymer.html">
+<link rel="import" href="../../views/namespace-list/component.html">
+
+<polymer-element name="p2b-neighborhood">
+  <template>
+    <link rel="stylesheet" href="../common/common.css">
+    <h2 page-title>Neighborhood</h2>
+    <p>List of PipeToBrowser instances currently published</p>
+    <p2b-namespace-list names="{{existingNames}}"></p2b-namespace-list>
+  </template>
+  <script>
+    Polymer('p2b-neighborhood', {
+      /*
+       * List of existing names to show
+       * @type {Array<string>}
+       */
+      existingNames: [],
+    });
+    </script>
+</polymer-element>
diff --git a/browser/views/neighborhood/view.js b/browser/views/neighborhood/view.js
new file mode 100644
index 0000000..5431d7f
--- /dev/null
+++ b/browser/views/neighborhood/view.js
@@ -0,0 +1,21 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View displaying a list of currently published PipeToBrowsers instances
+ * @class
+ * @extends {View}
+ */
+export class NeighborhoodView extends View {
+	constructor() {
+		var el = document.createElement('p2b-neighborhood');
+		super(el);
+	}
+
+ /*
+  * List of existing names to show
+  * @type {Array<string>}
+  */
+  set existingNames(val)  {
+    this.element.existingNames = val;
+  }
+}
diff --git a/browser/views/page/component.css b/browser/views/page/component.css
new file mode 100644
index 0000000..a151002
--- /dev/null
+++ b/browser/views/page/component.css
@@ -0,0 +1,46 @@
+
+[drawer] {
+  background-color: #FAFAFA;
+  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15);
+}
+
+[drawer] core-toolbar {
+  background-color: rgb(66, 133, 244);
+  color: #FAFAFA;
+}
+
+[sidebar] {
+  padding: 0.5em;
+}
+
+[sidebar] .core-selected {
+  color: #00bcd4;
+}
+
+[main] {
+  height: 100%;
+}
+
+[main] core-toolbar {
+  background-color: rgb(51, 103, 214);
+  color: #FAFAFA;
+}
+
+core-drawer-panel:not([narrow]) paper-icon-button[icon=menu] {
+  display: none;
+}
+
+.sub-page {
+  padding: 1.5em;
+  display: none;
+}
+
+.sub-page.core-selected {
+  display: block;
+}
+
+h1, h2 {
+  font-size: 1em;
+  margin: 0;
+  font-weight: normal;
+}
diff --git a/browser/views/page/component.html b/browser/views/page/component.html
new file mode 100644
index 0000000..acce916
--- /dev/null
+++ b/browser/views/page/component.html
@@ -0,0 +1,149 @@
+<!--TODO(aghassemi) These paths needs to be relative until issue:
+https://github.com/Polymer/vulcanize/pull/36 is merged
+otherwise they won't get vulcanized -->
+<link rel="import" href="../../third-party/polymer/polymer.html">
+<link rel="import" href="../../third-party/core-drawer-panel/core-drawer-panel.html">
+<link rel="import" href="../../third-party/core-header-panel/core-header-panel.html">
+<link rel="import" href="../../third-party/core-toolbar/core-toolbar.html">
+<link rel="import" href="../../third-party/core-menu/core-menu.html">
+<link rel="import" href="../../third-party/core-item/core-item.html">
+<link rel="import" href="../../third-party/core-selector/core-selector.html">
+<link rel="import" href="../../third-party/paper-icon-button/paper-icon-button.html">
+<link rel="import" href="../../third-party/paper-toast/paper-toast.html">
+<link rel="import" href="../../third-party/core-icons/hardware-icons.html">
+<link rel="import" href="../../third-party/core-icons/social-icons.html">
+<link rel="import" href="../../views/publish/component.html"/>
+<link rel="import" href="../../views/status/component.html"/>
+<link rel="import" href="../../views/error/component.html"/>
+<link rel="import" href="../../views/help/component.html"/>
+<link rel="import" href="../../views/loading/component.html"/>
+<link rel="import" href="../../views/pipes/component.html"/>
+<link rel="import" href="../../views/redirect-pipe-dialog/component.html"/>
+<link rel="import" href="../../views/neighborhood/component.html"/>
+<link rel="import" href="../../pipe-viewers/builtin/console/component.html"/>
+<link rel="import" href="../../pipe-viewers/builtin/console/panic/component.html"/>
+<link rel="import" href="../../pipe-viewers/builtin/git/status/component.html"/>
+<link rel="import" href="../../pipe-viewers/builtin/vlog/component.html"/>
+<link rel="import" href="../../libs/ui-components/blackhole/component.html"/>
+
+<link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'>
+
+<polymer-element name="p2b-page">
+  <template>
+    <link rel="stylesheet" href="../common/common.css">
+    <link rel="stylesheet" href="component.css">
+    <core-drawer-panel id="drawerPanel">
+      <core-header-panel drawer>
+        <core-toolbar>
+          <h1>Pipe To Browser</h1>
+        </core-toolbar>
+
+        <core-menu sidebar valueattr="key" selected="{{ selectedSubPageKey }}">
+          <template repeat="{{ subPage in subPages }}">
+            <core-item key="{{ subPage.key }}" icon="{{ subPage.icon }}" label="{{ subPage.name }}" on-click="{{ activateSubPage }}"></core-item>
+          </template>
+        </core-menu>
+
+      </core-header-panel>
+      <core-header-panel main>
+        <core-toolbar >
+          <paper-icon-button icon="menu" on-click="{{ toggleDrawer }}"></paper-icon-button>
+          <h2>{{ pageTitle }}</h2>
+        </core-toolbar>
+
+        <core-selector id="subPagesSelector" valueattr="key" selected="{{ selectedSubPageKey }}">
+         <template repeat="{{ subPage in subPages }}">
+            <div class="sub-page" key="{{ subPage.key }}"></div>
+         </template>
+        </core-selector>
+      </core-header-panel>
+    </core-drawer-panel>
+     <paper-toast id="toast"></paper-toast>
+  </template>
+  <script>
+    Polymer('p2b-page', {
+
+      /*
+       * Title of the page
+       * @type {string}
+       */
+      pageTitle: '',
+
+      /*
+       * SubPageItem represents top level sub pages that have a sidebar navigation link
+       * and a content area which gets displayed when corresponding sidebar item
+       * is activated by the end user.
+       * @type {object}
+       */
+      subPages: [],
+
+      /*
+       * Currently selected sub page's key
+       * @type {string}
+       */
+      selectedSubPageKey: '',
+
+      /*
+       * Sets the content of the sub page identified by its key and also selects the corresponding sidebar item for it
+       * @param {string} key Key of the sub page
+       * @param {DOMElement} el Element to become the content of the sub page
+       */
+      setSubPage: function(key, el) {
+        // TODO(aghassemi)
+        // This setTimeout is a work-around because template may not have been activated when this is called.
+        // Issue brought up with Polymer team.
+        var self = this;
+        setTimeout(function() {
+          var subPage = self.$.subPagesSelector.querySelector('div[key="' + key + '"]');
+          if(!subPage) {
+            return;
+          }
+          if(el.parentNode !== subPage) {
+            subPage.innerHTML = '';
+            subPage.appendChild(el);
+          }
+          self.selectedSubPageKey = key;
+        });
+      },
+
+     /*
+      * Displays a message toast for a few seconds e.g. "Saved Successfully"
+      * @param {string} text Text of the toast
+      */
+      showToast: function(text) {
+        this.$.toast.text = text;
+        this.$.toast.show();
+      },
+
+      /*
+       * handler for when a sidebar item is clicked
+       * @param {string} key Key of the page
+       * @private
+       */
+      activateSubPage: function(e) {
+        // find the targeted subPage item
+        var key = e.toElement.getAttribute('key');
+        var subPage = this.subPages.filter(function(s) {
+          return s.key === key;
+        })[0];
+
+        // call the sub page's delegate
+        if(subPage.onActivate) {
+          subPage.onActivate.call(subPage.onActivate);
+        }
+
+        // toggle the drawer
+        this.toggleDrawer();
+      },
+
+      /*
+       * toggles the drawer
+       * @private
+       */
+      toggleDrawer: function() {
+        this.$.drawerPanel.togglePanel();
+      }
+
+    });
+  </script>
+</polymer-element>
diff --git a/browser/views/page/view.js b/browser/views/page/view.js
new file mode 100644
index 0000000..e23e1ae
--- /dev/null
+++ b/browser/views/page/view.js
@@ -0,0 +1,79 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View representing the application page. Includes page level navigation, toolbar
+ * and other page chrome. Used as the container for all other views.
+ * @class
+ * @extends {View}
+ */
+export class PageView extends View {
+	constructor() {
+		var el = document.createElement('p2b-page');
+    el.subPages = [];
+		super(el);
+	}
+
+ /*
+  * Displays the given view inside the sub page area identified by the key.
+  * @param {String} subPageKey Key for the sub page to display.
+  * @param {View} view View to display.
+  */
+	setSubPageView(subPageKey, view) {
+		this.element.setSubPage(subPageKey, view.element);
+	}
+
+ /*
+  * Displayed a message toast for a few seconds e.g. "Saved Successfully"
+  * @type {SubPageItem}
+  */
+  showToast(text) {
+    this.element.showToast(text);
+  }
+
+ /*
+  * Collection of sub pages
+  * @type {SubPageItem}
+  */
+  get subPages() {
+    return this.element.subPages;
+  }
+
+ /*
+  * Title of the page
+  * @type {string}
+  */
+  set title(title) {
+    this.element.pageTitle = title;
+  }
+
+}
+
+/*
+ * SubPageItem represents top level sub pages that have a sidebar navigation link
+ * and a content area which gets displayed when corresponding sidebar item
+ * is activated by the end user.
+ * @param {String} key Unique identified for this sub page
+ * @class
+ */
+export class SubPageItem {
+  constructor(key) {
+    /*
+     * Name of the page. Normally displayed as the sidebar navigation item text
+     * @type {String}
+     */
+    this.name = name;
+
+    /*
+     * Unique identified for this sub page
+     * @type {String}
+     */
+    this.key = key;
+
+    /*
+     * Function that's called when user activates the sub page, normally by clicking
+     * the sidebar navigation items for the sub page
+     * @type {Function}
+     */
+    this.onActivate = null;
+  }
+}
diff --git a/browser/views/pipes/component.css b/browser/views/pipes/component.css
new file mode 100644
index 0000000..819c741
--- /dev/null
+++ b/browser/views/pipes/component.css
@@ -0,0 +1,39 @@
+#tabs {
+  background-color: #00bcd4;
+  color: #fafafa;
+  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15);
+  width: 100%;
+  z-index: 1;
+}
+
+#tabs > paper-tab:not(:last-child) {
+  border-right: solid 1px rgba(0, 0, 0, 0.1);
+}
+
+#tabPages {
+  position: relative;
+  overflow: hidden;
+}
+
+.no-pipes-bg {
+  background-image: url('factory.png');
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: 70% 50%;
+  height: 100%;
+  width: 100%;
+  opacity: 0.15;
+}
+
+.empty-message {
+  padding: 1em;
+  position: absolute;
+}
+
+.container {
+  position: absolute;
+  bottom: 0;
+  top: 0;
+  right: 0;
+  left: 0;
+}
diff --git a/browser/views/pipes/component.html b/browser/views/pipes/component.html
new file mode 100644
index 0000000..a2a2286
--- /dev/null
+++ b/browser/views/pipes/component.html
@@ -0,0 +1,180 @@
+<link rel="import" href="../../third-party/polymer/polymer.html">
+<link rel="import" href="../../third-party/core-pages/core-pages.html">
+<link rel="import" href="../../third-party/core-icons/core-icons.html">
+<link rel="import" href="../../third-party/core-icon-button/core-icon-button.html">
+<link rel="import" href="../../third-party/core-toolbar/core-toolbar.html">
+<link rel="import" href="../../third-party/core-selector/core-selector.html">
+<link rel="import" href="../../third-party/paper-tabs/paper-tabs.html">
+<link rel="import" href="tab-content/component.html">
+<link rel="import" href="tab-toolbar/component.html">
+
+<polymer-element name="p2b-pipes">
+  <template>
+    <link rel="stylesheet" href="../common/common.css">
+    <link rel="stylesheet" href="component.css">
+    <div class="container" flex>
+      <template if="{{ numTabs == 0 }}">
+        <h2 page-title class="empty-message">No pipes to show...</h2>
+        <div class="no-pipes-bg"></div>
+      </template>
+
+      <div id="tabsContainer" class="{{ {hidden : numTabs == 0} | tokenList}}" flex>
+        <paper-tabs id="tabs" class="{{ {hidden : numTabs <= 1} | tokenList}}" on-core-select="{{ handleTabSelectionChange }}" valueattr="key" selected="{{ selectedTabKey }}" noink></paper-tabs>
+        <core-selector id="tabPages" valueattr="key" selected="{{ selectedTabKey }}" flex></core-selector>
+      </div>
+    </div>
+  </template>
+  <script>
+    Polymer('p2b-pipes', {
+
+      /*
+       * Map of existing pipe tabs. Key is the tab key.
+       * @type {set}
+       * @private
+       */
+      pipeTabs: {},
+
+      /*
+       * Key of currently selected tab
+       * @type {string}
+       */
+      selectedTabKey : '',
+
+      /*
+       * Stack of previously selected tabs.
+       * @type {Array<string>}
+       * @private
+       */
+      selectionHistoryStack: [],
+
+      ready: function() {
+        this.numTabs = 0
+      },
+
+      /*
+       * Adds a new tab
+       * @param {string} key Key of the tab to add
+       * @param {string} name Name of the tab to add
+       * @param {DOMElement} el Content of the tab
+       * @param {function} onClose Optional onClose callback.
+       */
+      addTab: function(key, name, el, onClose) {
+        var self = this;
+
+        // Create a tab thumb
+        var tab = document.createElement('paper-tab');
+        tab.key = key;
+        tab.textContent = name;
+
+        // Create a tab toolbar and assign the close handler
+        var tabToolbar = document.createElement('p2b-pipes-tab-toolbar');
+        tabToolbar.toolbarTitle = name;
+        tabToolbar.addEventListener('close-action', function() {
+          self.removeTab(key);
+          if (onClose) {
+            onClose();
+          }
+        });
+        tabToolbar.addEventListener('fullscreen-action', function() {
+          var tabContent = self.pipeTabs[key].tabContent;
+          tabContent.fullscreen();
+        });
+
+        // Create the content of the tab
+        var tabContent = document.createElement('p2b-pipes-tab-content');
+        tabContent.setAttribute('key', key);
+        tabContent.appendChild(tabToolbar);
+        tabContent.appendChild(el);
+
+        this.$.tabPages.appendChild(tabContent);
+
+        // Add the tab to our list.
+        this.pipeTabs[key] = {
+          name: name,
+          tab: tab,
+          tabContent: tabContent,
+          tabToolbar: tabToolbar
+        };
+
+        this.numTabs++;
+
+        this.selectedTabKey = key;
+        requestAnimationFrame(function() {
+          self.$.tabs.appendChild(tab);
+        });
+      },
+
+      /*
+       * Adds a new toolbar action for the tab's toolbar
+       * @param {string} tabKey Key of the tab to add action to
+       * @param icon {string} icon Icon name for the action
+       * @param onClick {function} event handler for the action
+       */
+      addToolbarAction: function(tabKey, icon, onClick) {
+        if (!this.pipeTabs[tabKey]) {
+          return;
+        }
+        var toolbar = this.pipeTabs[tabKey].tabToolbar;
+        toolbar.add(icon, onClick);
+      },
+
+      /*
+       * Removes a tab
+       * @param {string} key Key of the tab to remove
+       */
+      removeTab: function(key) {
+        if (!this.pipeTabs[key]) {
+          return;
+        }
+        // Remove tab thumb and content
+        var tab = this.pipeTabs[key].tab;
+        tab.remove();
+        var tabContent = this.pipeTabs[key].tabContent;
+        tabContent.remove();
+
+        // Delete tab from the map
+        delete this.pipeTabs[key];
+        this.numTabs--;
+
+        // Select an existing tab from previous selection history
+        var toSelect = this.selectionHistoryStack.pop();
+        while ( toSelect && !this.pipeTabs[toSelect] ) {
+          // pop until we find one that still exists
+          toSelect = this.selectionHistoryStack.pop();
+        }
+        if (toSelect) {
+          this.selectedTabKey = toSelect;
+        }
+      },
+
+      /*
+       * Replaces content of a tab
+       * @param {string} key Key of the tab to replace content for
+       * @param {string} newName new name for the tab
+       * @param {DOMElement} el New content of the tab
+       */
+      replaceTabContent: function(key, newName, newEl) {
+        if (!this.pipeTabs[key]) {
+          return;
+        }
+        var tabContent = this.pipeTabs[key].tabContent;
+        tabContent.replaceTabContent(newEl);
+        if (newName) {
+          this.pipeTabs[key].tab.textContent = newName;
+          this.pipeTabs[key].tabToolbar.toolbarTitle = newName;
+        }
+      },
+
+      /*
+       * Adds the tab selection to history when selection changes
+       * @private
+       */
+      handleTabSelectionChange: function(e) {
+        if (!e.detail.isSelected){
+          return;
+        }
+        this.selectionHistoryStack.push(this.selectedTabKey);
+      }
+    });
+  </script>
+</polymer-element>
diff --git a/browser/views/pipes/factory.png b/browser/views/pipes/factory.png
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/browser/views/pipes/factory.png
diff --git a/browser/views/pipes/tab-content/component.css b/browser/views/pipes/tab-content/component.css
new file mode 100644
index 0000000..d3eea80
--- /dev/null
+++ b/browser/views/pipes/tab-content/component.css
@@ -0,0 +1,25 @@
+/* become offset parent for content container */
+.tab-main {
+  position: relative;
+}
+
+/* content fills remaining space */
+.tab-main-content {
+  overflow: auto;
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+/* hidden, unless selected */
+:host(.core-selected)  {
+  display: flex !important;
+}
+
+:host {
+  display: none !important;
+}
+
+
diff --git a/browser/views/pipes/tab-content/component.html b/browser/views/pipes/tab-content/component.html
new file mode 100644
index 0000000..60047ab
--- /dev/null
+++ b/browser/views/pipes/tab-content/component.html
@@ -0,0 +1,40 @@
+<link rel="import" href="../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../third-party/core-toolbar/core-toolbar.html">
+
+<polymer-element name="p2b-pipes-tab-content" flex>
+  <template>
+    <link rel="stylesheet" href="component.css">
+    <content select="p2b-pipes-tab-toolbar"></content>
+    <div class="tab-main" flex>
+      <div id="main" class="tab-main-content" flex>
+        <content></content>
+      </div>
+    </div>
+  </template>
+  <script>
+    Polymer('p2b-pipes-tab-content', {
+
+      /*
+       * Replaces existing content of the tab with the new element
+       * @param {DOMElement} new element to replace existing content
+       */
+      replaceTabContent: function(newEl) {
+        //TODO(aghassemi) There must be a better way for these .innerHTML='', figure it out.
+        this.$.main.innerHTML = '';
+        this.$.main.appendChild(newEl);
+      },
+
+      /*
+       * Puts the tab content into fullscreen mode
+       */
+      fullscreen: function() {
+        var flag = Element.ALLOW_KEYBOARD_INPUT;
+        if (this.$.main.requestFullscreen) {
+          this.$.main.requestFullscreen(flag);
+        } else if( this.$.main.webkitRequestFullscreen ) {
+          this.$.main.webkitRequestFullscreen(flag);
+        }
+      }
+    });
+  </script>
+</polymer-element>
diff --git a/browser/views/pipes/tab-toolbar/component.css b/browser/views/pipes/tab-toolbar/component.css
new file mode 100644
index 0000000..9abe0b1
--- /dev/null
+++ b/browser/views/pipes/tab-toolbar/component.css
@@ -0,0 +1,11 @@
+core-toolbar {
+  background-color: rgba(0, 0, 0, 0.10);
+  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15);
+  z-index: 1;
+  font-size: 1em;
+  height: 48px;
+}
+
+core-toolbar::shadow .toolbar-tools {
+  height: 48px;
+}
diff --git a/browser/views/pipes/tab-toolbar/component.html b/browser/views/pipes/tab-toolbar/component.html
new file mode 100644
index 0000000..6a2fc42
--- /dev/null
+++ b/browser/views/pipes/tab-toolbar/component.html
@@ -0,0 +1,54 @@
+<link rel="import" href="../../../third-party/polymer/polymer.html">
+<link rel="import" href="../../../third-party/paper-icon-button/paper-icon-button.html">
+
+<polymer-element name="p2b-pipes-tab-toolbar">
+  <template>
+    <link rel="stylesheet" href="component.css">
+    <core-toolbar>
+      <span flex>
+        {{ toolbarTitle }}
+      </span>
+      <span id="customActions"></span>
+      <paper-icon-button id="fullscreenIcon" icon="fullscreen" on-click="{{ fireFullscreenAction }}"></paper-icon-button>
+      <paper-icon-button icon="close" on-click="{{ fireCloseAction }}"></paper-icon-button>
+    </core-toolbar>
+  </template>
+  <script>
+    Polymer('p2b-pipes-tab-toolbar', {
+
+      /*
+       * Title of the toolbar
+       * @type {string}
+       */
+      toolbarTitle: '',
+
+      /*
+       * Event that's fired when close action of the toolbar is triggered
+       * @event
+       */
+      fireCloseAction: function() {
+        this.fire('close-action');
+      },
+
+      /*
+       * Event that's fired when fullscreen action of the toolbar is triggered
+       * @event
+       */
+      fireFullscreenAction: function() {
+        this.fire('fullscreen-action');
+      },
+
+      /*
+       * Adds a new action to the toolbar
+       * @param icon {string} icon Icon for the action
+       * @param onClick {function} event handler for the action
+       */
+      add: function(icon, onClick) {
+        var button = document.createElement('paper-icon-button');
+        button.icon = icon;
+        button.addEventListener('click', onClick);
+        this.$.customActions.appendChild(button);
+      }
+    });
+  </script>
+</polymer-element>
diff --git a/browser/views/pipes/view.js b/browser/views/pipes/view.js
new file mode 100644
index 0000000..78fcd20
--- /dev/null
+++ b/browser/views/pipes/view.js
@@ -0,0 +1,46 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View representing a collection of pipes displayed in tabs.
+ * this view manages the tabs and the empty message when no pipes available
+ * @class
+ * @extends {View}
+ */
+export class PipesView extends View {
+  constructor() {
+    var el = document.createElement('p2b-pipes');
+    super(el);
+  }
+
+ /*
+  * Adds the given view as a new pipe viewer tab
+  * @param {string} key A string key identifier for the tab.
+  * @param {string} name A short name for the tab that will be displayed as
+  * the tab title
+  * @param {View} view View to show inside the tab.
+  * @param {function} onClose Optional onClose callback.
+  */
+  addTab(key, name, view, onClose) {
+    this.element.addTab(key, name, view.element, onClose);
+  }
+
+  /*
+   * Adds a new toolbar action for the tab's toolbar
+   * @param {string} key Key of the tab to add action to
+   * @param icon {string} icon key for the action
+   * @param onClick {function} event handler for the action
+   */
+  addToolbarAction(tabKey, icon, onClick) {
+    this.element.addToolbarAction(tabKey, icon, onClick);
+  }
+
+ /*
+  * Replaces the content of the tab identified via key by the new view.
+  * @param {string} key A string key identifier for the tab.
+  * @param {string} newName New name for the tab
+  * @param {View} view View to replace the current tab content
+  */
+  replaceTabView(key, newName, newView) {
+    this.element.replaceTabContent(key, newName, newView.element);
+  }
+}
diff --git a/browser/views/publish/component.css b/browser/views/publish/component.css
new file mode 100644
index 0000000..4249a0f
--- /dev/null
+++ b/browser/views/publish/component.css
@@ -0,0 +1,4 @@
+paper-input {
+  width: 100%;
+  max-width: 30em;
+}
diff --git a/browser/views/publish/component.html b/browser/views/publish/component.html
new file mode 100644
index 0000000..95095f7
--- /dev/null
+++ b/browser/views/publish/component.html
@@ -0,0 +1,32 @@
+<link rel="import" href="../../third-party/polymer/polymer.html">
+<link rel="import" href="../../third-party/paper-input/paper-input.html">
+<link rel="import" href="../../third-party/paper-button/paper-button.html">
+
+<polymer-element name="p2b-publish" attributes="publishState">
+
+  <template id="template">
+    <link rel="stylesheet" href="../common/common.css">
+    <link rel="stylesheet" href="component.css">
+    <paper-input id="publishNameInput" label="Name to publish under (e.g. john-tablet)" error="You must pick a name!" floatinglabel/></paper-input>
+    <paper-button class="paper colored" inkColor="#3367d6" on-click="{{ publishAction }}">Publish</paper-button>
+  </template>
+  <script>
+    Polymer('p2b-publish', {
+
+      /*
+       * Publish action. Fires when user decided to publish the p2b service.
+       * user-entered name of the service will be provided as value of the event
+       * @event
+       */
+      publishAction: function() {
+        var name = this.$.publishNameInput.value.trim();
+        if(name === "") {
+          this.$.publishNameInput.invalid = true;
+          this.$.publishNameInput.classList.toggle('invalid', true);
+          return;
+        }
+        this.fire('publish', { publishName: name  });
+      }
+    });
+  </script>
+</polymer-element>
diff --git a/browser/views/publish/view.js b/browser/views/publish/view.js
new file mode 100644
index 0000000..3318d51
--- /dev/null
+++ b/browser/views/publish/view.js
@@ -0,0 +1,25 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View representing the state and interaction for publishing the p2b service.
+ * @class
+ * @extends {View}
+ */
+export class PublishView extends View {
+  constructor(publishState) {
+    var el = document.createElement('p2b-publish');
+    el.publishState = publishState;
+    super(el);
+  }
+
+/*
+ * Event representing user's intention to publish the p2b service under the provided name
+ * @event
+ * @type {string} Requested name for service to be published under
+ */
+  onPublishAction(eventHandler) {
+    this.element.addEventListener('publish', (e) => {
+      eventHandler(e.detail.publishName);
+    });
+  }
+}
diff --git a/browser/views/redirect-pipe-dialog/component.css b/browser/views/redirect-pipe-dialog/component.css
new file mode 100644
index 0000000..3c27e35
--- /dev/null
+++ b/browser/views/redirect-pipe-dialog/component.css
@@ -0,0 +1,22 @@
+paper-input {
+  width: 90%;
+}
+
+paper-input, paper-checkbox {
+  display: block;
+}
+
+paper-dialog {
+  max-width: 40em;
+  width: 80vw;
+}
+
+paper-button[affirmative] {
+  color: #4285f4;
+}
+
+.label {
+  margin: 0;
+  margin-top: 1em;
+  font-size: 1.1em;
+}
diff --git a/browser/views/redirect-pipe-dialog/component.html b/browser/views/redirect-pipe-dialog/component.html
new file mode 100644
index 0000000..d6164a3
--- /dev/null
+++ b/browser/views/redirect-pipe-dialog/component.html
@@ -0,0 +1,78 @@
+<link rel="import" href="../../third-party/polymer/polymer.html">
+<link rel="import" href="../../third-party/paper-input/paper-input.html">
+<link rel="import" href="../../third-party/paper-button/paper-button.html">
+<link rel="import" href="../../third-party/paper-dialog/paper-dialog.html">
+<link rel="import" href="../../third-party/paper-dialog/paper-dialog-transition.html">
+<link rel="import" href="../../views/namespace-list/component.html">
+
+<polymer-element name="p2b-redirect-pipe-dialog">
+
+  <template id="template">
+    <link rel="stylesheet" href="../common/common.css">
+    <link rel="stylesheet" href="component.css">
+    <paper-dialog id="dialog" heading="Redirect" transition="paper-dialog-transition-bottom">
+      <p>
+        <paper-input focused id="nameInput" label="Name to redirect to" floatinglabel></paper-input>
+        <paper-checkbox id="newDataOnly" label="Only redirect new data"></paper-checkbox>
+        <template if="{{existingNames.length > 0}}">
+          <h2 class="label">Currently online</h2>
+          <p2b-namespace-list selectable on-select="{{updateNameInput}}" names="{{existingNames}}"></p2b-namespace-list>
+        </template>
+      </p>
+      <paper-button label="Cancel" dismissive></paper-button>
+      <paper-button label="Redirect" affirmative default on-tap="{{ fireRedirectActionEvent }}"></paper-button>
+    </paper-dialog>
+  </template>
+  <script>
+    Polymer('p2b-redirect-pipe-dialog', {
+
+      /*
+       * List of existing names to show in the dialog for the user to pick from
+       * @type {Array<string>}
+       */
+      existingNames: [],
+
+      ready: function() {
+        var self = this;
+        var dialog = this.$.dialog;
+        var container = document.querySelector('#redirectDialogContainer');
+        if (!container) {
+          var container = document.createElement('div');
+          container.id = 'redirectDialogContainer';
+          document.body.appendChild(container);
+        }
+        this.container = container;
+      },
+
+      /*
+       * Opens the dialog
+       */
+      open: function() {
+        this.container.innerHTML = ''
+        this.container.appendChild(this);
+        this.$.dialog.toggle();
+      },
+
+      /*
+       * Fires redirect event representing user's intention to redirect
+       * @type {string} Requested name for service to be redirected
+       * @type {boolean} Whether only new data should be redirected
+       */
+      fireRedirectActionEvent: function() {
+        var name = this.$.nameInput.value;
+        this.fire('redirect', {
+          name: name,
+          newDataOnly: this.$.newDataOnly.checked
+        });
+      },
+
+      /*
+       * Updates the input value
+       * @private
+       */
+      updateNameInput: function(e) {
+        this.$.nameInput.value = e.detail;
+      }
+    });
+  </script>
+</polymer-element>
diff --git a/browser/views/redirect-pipe-dialog/view.js b/browser/views/redirect-pipe-dialog/view.js
new file mode 100644
index 0000000..f8c5771
--- /dev/null
+++ b/browser/views/redirect-pipe-dialog/view.js
@@ -0,0 +1,42 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View representing a dialog that asks the user where they want to redirect
+ * the current pipe and whether only new data should be redirected
+ * @class
+ * @extends {View}
+ */
+export class RedirectPipeDialogView extends View {
+  constructor() {
+    var el = document.createElement('p2b-redirect-pipe-dialog');
+    super(el);
+  }
+
+  /*
+   * Opens the Redirect Pipe Dialog
+   */
+  open() {
+    this.element.open();
+  }
+
+  /*
+   * List of existing names to show in the dialog for the user to pick from
+   * @type {Array<string>}
+   */
+  set existingNames(val) {
+    this.element.existingNames = val;
+  }
+
+ /*
+  * Event representing user's intention to redirect
+  * @event
+  * @type {string} Requested name for service to be redirected
+  * @type {boolean} Whether only new data should be redirected
+  */
+  onRedirectAction(eventHandler) {
+    this.element.addEventListener('redirect', (e) => {
+      eventHandler(e.detail.name, e.detail.newDataOnly);
+    });
+  }
+
+}
diff --git a/browser/views/status/component.css b/browser/views/status/component.css
new file mode 100644
index 0000000..c339c33
--- /dev/null
+++ b/browser/views/status/component.css
@@ -0,0 +1,14 @@
+h3 {
+  font-size: 1.0em;
+  padding-bottom: 0.2em;
+  color: #4285f4;
+  border-bottom: 1px solid rgba(0,0,0,0.05);
+  font-weight: normal;
+  text-transform: uppercase;
+  margin: 0;
+}
+
+p {
+  margin-top: 0.2em;
+  margin-bottom: 1.5em;
+}
diff --git a/browser/views/status/component.html b/browser/views/status/component.html
new file mode 100644
index 0000000..037731f
--- /dev/null
+++ b/browser/views/status/component.html
@@ -0,0 +1,102 @@
+<link rel="import" href="../../third-party/polymer/polymer.html">
+<link rel="import" href="../../third-party/paper-button/paper-button.html">
+
+<polymer-element name="p2b-status" attributes="status">
+
+  <template>
+    <link rel="stylesheet" href="../common/common.css">
+    <link rel="stylesheet" href="component.css">
+    <h3>Status</h3>
+    <p>{{ serviceState | formatServiceState }}</p>
+    <div class="{{ {hidden : !serviceState.published} | tokenList }}">
+      <h3>Name</h3>
+      <p>{{ serviceState.fullServiceName }}</p>
+
+      <h3>Published on</h3>
+      <p>{{ serviceState.date | formatDate }}</p>
+
+      <h3>Running Since</h3>
+      <p>{{ runningSince }}</p>
+
+      <h3>Number of pipe requests</h3>
+      <p>{{ serviceState.numPipes | formatInteger }}</p>
+
+      <h3>Total bytes received</h3>
+      <p>{{ serviceState.numBytes | formatBytes }}</p>
+    </div>
+    <paper-button class="paper colored red" inkColor="#A9352C" on-click="{{ stopAction }}">Stop</paper-button>
+  </template>
+  <script>
+    System.import('libs/utils/formatting').then(function(formatter) {
+      Polymer('p2b-status', {
+
+        ready: function() {
+          this.runningSince = 'just now';
+        },
+
+        attached: function() {
+          // Update the running since every second.
+          this.runningSinceIntervalId = setInterval(this.updateRunningSince.bind(this), 1000);
+        },
+
+        detached: function() {
+          clearInterval(this.runningSinceIntervalId);
+        },
+
+        /*
+         * Dynamic binding for the state of publishing p2b service.
+         * Any changes to this object will be reflected in the UI automatically
+         */
+        serviceState: null,
+
+        /*
+         * Human friendly formatting functions. Because polymer filter expressions
+         * don't accept obj.func we wrap them here
+         * @private
+         */
+        formatDate: formatter.formatDate,
+        formatInteger: formatter.formatInteger,
+        formatBytes: formatter.formatBytes,
+
+        /*
+         * Auto-updating Uptime text
+         * @private
+         * @type {string}
+         */
+        updateRunningSince: function() {
+          if (!this.serviceState) { return; }
+          this.runningSince = formatter.formatRelativeTime(this.serviceState.date);
+        },
+
+        /*
+         * Status text
+         * @private
+         * @type {string}
+         */
+        formatServiceState: function(serviceState) {
+          if (!serviceState) {
+            return '';
+          }
+          if (serviceState.published) {
+            return 'Published';
+          } else if(serviceState.publishing) {
+            return 'Publishing';
+          } else if(serviceState.stopping) {
+            return 'Stopping';
+          }  else {
+            return 'Stopped';
+          }
+        },
+
+        /*
+         * Stop action. Fires when user decides to stop the p2b service.
+         * @event
+         */
+        stopAction: function() {
+          this.fire('stop');
+        }
+
+      });
+    });
+  </script>
+</polymer-element>
diff --git a/browser/views/status/view.js b/browser/views/status/view.js
new file mode 100644
index 0000000..a65283d
--- /dev/null
+++ b/browser/views/status/view.js
@@ -0,0 +1,24 @@
+import { View } from 'libs/mvc/view'
+
+/*
+ * View representing the state and interaction for publishing the p2b service.
+ * @class
+ * @extends {View}
+ */
+export class StatusView extends View {
+	constructor(serviceState) {
+		var el = document.createElement('p2b-status');
+		el.serviceState = serviceState;
+		super(el);
+	}
+
+/*
+ * Event representing user's intention to stop the published service
+ * @event
+ */
+  onStopAction(eventHandler) {
+    this.element.addEventListener('stop', () => {
+      eventHandler();
+    });
+  }
+}
diff --git a/go/src/p2b/main.go b/go/src/p2b/main.go
new file mode 100644
index 0000000..e9f7e20
--- /dev/null
+++ b/go/src/p2b/main.go
@@ -0,0 +1,101 @@
+// Command p2b is a client for the pipetobrowser service. It pipes its
+// standard input to a pipetobrowser service.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"os"
+
+	_ "v.io/core/veyron/profiles/static"
+	"v.io/core/veyron2"
+	"v.io/core/veyron2/vlog"
+
+	"p2b/vdl"
+)
+
+const usage = `
+%s is a Pipe To Browser client. It allows one to pipe any stdout stream from console to the browser.
+Data being piped to the browser then is displayed in a graphical and formatted way by a "viewer".
+
+Usage:
+
+  %s [<name>/<viewer>]
+
+  For example:
+
+	ls -l | p2b google/p2b/jane/console
+
+	or
+
+	cat cat.jpg | p2b google/p2b/jane/image
+
+  where <name> (google/p2b/jane) is the object name where p2b
+  service is running in the browser. <viewer> (console, image) specifies what
+  viewer should be used to display the data.
+
+  To redirect stderr of a process, in *nix system you can use 2>&1 before piping to P2B.
+
+  For example many daemons may write log lines to stderr instead of stdout:
+
+  serverd -alsologtostderr=true 2>&1 | google/p2b/jane/vlog
+`
+
+func Usage() {
+	fmt.Fprintf(os.Stdout, usage, os.Args[0], os.Args[0])
+}
+
+type sender interface {
+	Send(p []byte) error
+}
+
+// viewerPipeStreamWriter adapts ViewerPipeStream to io.Writer
+type viewerPipeStreamWriter struct {
+	sender
+}
+
+func (w viewerPipeStreamWriter) Write(p []byte) (n int, err error) {
+	w.Send(p)
+	return len(p), nil
+}
+
+func main() {
+	flag.Parse()
+	flag.Usage = Usage
+
+	if flag.NArg() != 1 {
+		Usage()
+		return
+	}
+
+	ctx, shutdown := veyron2.Init()
+	defer shutdown()
+
+	name := flag.Arg(0)
+
+	// bind to the p2b service
+	s := vdl.ViewerClient(name)
+
+	stream, err := s.Pipe(ctx)
+	if err != nil {
+		vlog.Errorf("failed to pipe to '%s' please ensure p2b service is running in the browser and name is correct.\nERR:%v", name, err)
+		return
+	}
+
+	w := viewerPipeStreamWriter{stream.SendStream()}
+
+	_, err = io.Copy(w, os.Stdin)
+	if err != nil {
+		vlog.Errorf("failed to copy the stdin pipe to the outgoing stream\nERR:%v", err)
+		return
+	}
+
+	_, err = stream.Finish()
+	if err != nil {
+		vlog.Errorf("error finishing stream: %v", err)
+		return
+	}
+
+	fmt.Println("Finished piping to browser! Thanks for using p2b.")
+}
diff --git a/go/src/p2b/vdl/p2b.vdl b/go/src/p2b/vdl/p2b.vdl
new file mode 100644
index 0000000..7b9fc1d
--- /dev/null
+++ b/go/src/p2b/vdl/p2b.vdl
@@ -0,0 +1,13 @@
+// Package vdl is an example of a veyron service for
+// streaming data from a pipe to a browser, which can visualize this
+// data.
+package vdl
+
+// Viewer allows clients to stream data to it and to request a
+// particular viewer to format and display the data.
+type Viewer interface {
+  // Pipe creates a bidirectional pipe between client and viewer
+  // service, returns total number of bytes received by the service
+  // after streaming ends
+  Pipe() stream<[]byte, _> (any | error)
+}
diff --git a/go/src/p2b/vdl/p2b.vdl.go b/go/src/p2b/vdl/p2b.vdl.go
new file mode 100644
index 0000000..bf60aba
--- /dev/null
+++ b/go/src/p2b/vdl/p2b.vdl.go
@@ -0,0 +1,277 @@
+// This file was auto-generated by the veyron vdl tool.
+// Source: p2b.vdl
+
+// Package vdl is an example of a veyron service for
+// streaming data from a pipe to a browser, which can visualize this
+// data.
+package vdl
+
+import (
+	// VDL system imports
+	"io"
+	"v.io/core/veyron2"
+	"v.io/core/veyron2/context"
+	"v.io/core/veyron2/ipc"
+	"v.io/core/veyron2/vdl"
+)
+
+// ViewerClientMethods is the client interface
+// containing Viewer methods.
+//
+// Viewer allows clients to stream data to it and to request a
+// particular viewer to format and display the data.
+type ViewerClientMethods interface {
+	// Pipe creates a bidirectional pipe between client and viewer
+	// service, returns total number of bytes received by the service
+	// after streaming ends
+	Pipe(*context.T, ...ipc.CallOpt) (ViewerPipeCall, error)
+}
+
+// ViewerClientStub adds universal methods to ViewerClientMethods.
+type ViewerClientStub interface {
+	ViewerClientMethods
+	ipc.UniversalServiceMethods
+}
+
+// ViewerClient returns a client stub for Viewer.
+func ViewerClient(name string, opts ...ipc.BindOpt) ViewerClientStub {
+	var client ipc.Client
+	for _, opt := range opts {
+		if clientOpt, ok := opt.(ipc.Client); ok {
+			client = clientOpt
+		}
+	}
+	return implViewerClientStub{name, client}
+}
+
+type implViewerClientStub struct {
+	name   string
+	client ipc.Client
+}
+
+func (c implViewerClientStub) c(ctx *context.T) ipc.Client {
+	if c.client != nil {
+		return c.client
+	}
+	return veyron2.GetClient(ctx)
+}
+
+func (c implViewerClientStub) Pipe(ctx *context.T, opts ...ipc.CallOpt) (ocall ViewerPipeCall, err error) {
+	var call ipc.Call
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "Pipe", nil, opts...); err != nil {
+		return
+	}
+	ocall = &implViewerPipeCall{Call: call}
+	return
+}
+
+// ViewerPipeClientStream is the client stream for Viewer.Pipe.
+type ViewerPipeClientStream interface {
+	// SendStream returns the send side of the Viewer.Pipe client stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors
+		// encountered while sending, or if Send is called after Close or
+		// the stream has been canceled.  Blocks if there is no buffer
+		// space; will unblock when buffer space is available or after
+		// the stream has been canceled.
+		Send(item []byte) error
+		// Close indicates to the server that no more items will be sent;
+		// server Recv calls will receive io.EOF after all sent items.
+		// This is an optional call - e.g. a client might call Close if it
+		// needs to continue receiving items from the server after it's
+		// done sending.  Returns errors encountered while closing, or if
+		// Close is called after the stream has been canceled.  Like Send,
+		// blocks if there is no buffer space available.
+		Close() error
+	}
+}
+
+// ViewerPipeCall represents the call returned from Viewer.Pipe.
+type ViewerPipeCall interface {
+	ViewerPipeClientStream
+	// Finish performs the equivalent of SendStream().Close, then blocks until
+	// the server is done, and returns the positional return values for the call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() (vdl.AnyRep, error)
+}
+
+type implViewerPipeCall struct {
+	ipc.Call
+}
+
+func (c *implViewerPipeCall) SendStream() interface {
+	Send(item []byte) error
+	Close() error
+} {
+	return implViewerPipeCallSend{c}
+}
+
+type implViewerPipeCallSend struct {
+	c *implViewerPipeCall
+}
+
+func (c implViewerPipeCallSend) Send(item []byte) error {
+	return c.c.Send(item)
+}
+func (c implViewerPipeCallSend) Close() error {
+	return c.c.CloseSend()
+}
+func (c *implViewerPipeCall) Finish() (o0 vdl.AnyRep, err error) {
+	if ierr := c.Call.Finish(&o0, &err); ierr != nil {
+		err = ierr
+	}
+	return
+}
+
+// ViewerServerMethods is the interface a server writer
+// implements for Viewer.
+//
+// Viewer allows clients to stream data to it and to request a
+// particular viewer to format and display the data.
+type ViewerServerMethods interface {
+	// Pipe creates a bidirectional pipe between client and viewer
+	// service, returns total number of bytes received by the service
+	// after streaming ends
+	Pipe(ViewerPipeContext) (vdl.AnyRep, error)
+}
+
+// ViewerServerStubMethods is the server interface containing
+// Viewer methods, as expected by ipc.Server.
+// The only difference between this interface and ViewerServerMethods
+// is the streaming methods.
+type ViewerServerStubMethods interface {
+	// Pipe creates a bidirectional pipe between client and viewer
+	// service, returns total number of bytes received by the service
+	// after streaming ends
+	Pipe(*ViewerPipeContextStub) (vdl.AnyRep, error)
+}
+
+// ViewerServerStub adds universal methods to ViewerServerStubMethods.
+type ViewerServerStub interface {
+	ViewerServerStubMethods
+	// Describe the Viewer interfaces.
+	Describe__() []ipc.InterfaceDesc
+}
+
+// ViewerServer returns a server stub for Viewer.
+// It converts an implementation of ViewerServerMethods into
+// an object that may be used by ipc.Server.
+func ViewerServer(impl ViewerServerMethods) ViewerServerStub {
+	stub := implViewerServerStub{
+		impl: impl,
+	}
+	// Initialize GlobState; always check the stub itself first, to handle the
+	// case where the user has the Glob method defined in their VDL source.
+	if gs := ipc.NewGlobState(stub); gs != nil {
+		stub.gs = gs
+	} else if gs := ipc.NewGlobState(impl); gs != nil {
+		stub.gs = gs
+	}
+	return stub
+}
+
+type implViewerServerStub struct {
+	impl ViewerServerMethods
+	gs   *ipc.GlobState
+}
+
+func (s implViewerServerStub) Pipe(ctx *ViewerPipeContextStub) (vdl.AnyRep, error) {
+	return s.impl.Pipe(ctx)
+}
+
+func (s implViewerServerStub) Globber() *ipc.GlobState {
+	return s.gs
+}
+
+func (s implViewerServerStub) Describe__() []ipc.InterfaceDesc {
+	return []ipc.InterfaceDesc{ViewerDesc}
+}
+
+// ViewerDesc describes the Viewer interface.
+var ViewerDesc ipc.InterfaceDesc = descViewer
+
+// descViewer hides the desc to keep godoc clean.
+var descViewer = ipc.InterfaceDesc{
+	Name:    "Viewer",
+	PkgPath: "p2b/vdl",
+	Doc:     "// Viewer allows clients to stream data to it and to request a\n// particular viewer to format and display the data.",
+	Methods: []ipc.MethodDesc{
+		{
+			Name: "Pipe",
+			Doc:  "// Pipe creates a bidirectional pipe between client and viewer\n// service, returns total number of bytes received by the service\n// after streaming ends",
+			OutArgs: []ipc.ArgDesc{
+				{"", ``}, // vdl.AnyRep
+				{"", ``}, // error
+			},
+		},
+	},
+}
+
+// ViewerPipeServerStream is the server stream for Viewer.Pipe.
+type ViewerPipeServerStream interface {
+	// RecvStream returns the receiver side of the Viewer.Pipe server stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() []byte
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+}
+
+// ViewerPipeContext represents the context passed to Viewer.Pipe.
+type ViewerPipeContext interface {
+	ipc.ServerContext
+	ViewerPipeServerStream
+}
+
+// ViewerPipeContextStub is a wrapper that converts ipc.ServerCall into
+// a typesafe stub that implements ViewerPipeContext.
+type ViewerPipeContextStub struct {
+	ipc.ServerCall
+	valRecv []byte
+	errRecv error
+}
+
+// Init initializes ViewerPipeContextStub from ipc.ServerCall.
+func (s *ViewerPipeContextStub) Init(call ipc.ServerCall) {
+	s.ServerCall = call
+}
+
+// RecvStream returns the receiver side of the Viewer.Pipe server stream.
+func (s *ViewerPipeContextStub) RecvStream() interface {
+	Advance() bool
+	Value() []byte
+	Err() error
+} {
+	return implViewerPipeContextRecv{s}
+}
+
+type implViewerPipeContextRecv struct {
+	s *ViewerPipeContextStub
+}
+
+func (s implViewerPipeContextRecv) Advance() bool {
+	s.s.errRecv = s.s.Recv(&s.s.valRecv)
+	return s.s.errRecv == nil
+}
+func (s implViewerPipeContextRecv) Value() []byte {
+	return s.s.valRecv
+}
+func (s implViewerPipeContextRecv) Err() error {
+	if s.s.errRecv == io.EOF {
+		return nil
+	}
+	return s.s.errRecv
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0209ac6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+  "name": "pipe-to-browser",
+  "version": "0.0.1",
+  "description": "P2B allows one to pipe anything from shell console to the browser. Data being piped to the browser then is displayed in a graphical and formatted way by a 'viewer' Viewers are pluggable pieces of code that know how to handle and display a stream of data.",
+  "devDependencies": {
+    "jspm": "0.12.0",
+    "vulcanize": "~0.7.9",
+    "serve": "~1.4.0",
+    "bower": "~1.3.12"
+  },
+  "jspm": {
+    "directories": {
+      "lib": "lib"
+    }
+  }
+}