diff --git a/src/applications/console/core/DarkConsoleCore.php b/src/applications/console/core/DarkConsoleCore.php
index ddb22b9cfe..437d6f0be7 100644
--- a/src/applications/console/core/DarkConsoleCore.php
+++ b/src/applications/console/core/DarkConsoleCore.php
@@ -1,136 +1,138 @@
 <?php
 
 final class DarkConsoleCore extends Phobject {
 
   private $plugins = array();
   const STORAGE_VERSION = 1;
 
   public function __construct() {
     $this->plugins = id(new PhutilClassMapQuery())
       ->setAncestorClass('DarkConsolePlugin')
       ->execute();
 
     foreach ($this->plugins as $plugin) {
       $plugin->setConsoleCore($this);
       $plugin->didStartup();
     }
   }
 
   public function getPlugins() {
     return $this->plugins;
   }
 
   public function getKey(AphrontRequest $request) {
     $plugins = $this->getPlugins();
 
     foreach ($plugins as $plugin) {
       $plugin->setRequest($request);
       $plugin->willShutdown();
     }
 
     foreach ($plugins as $plugin) {
       $plugin->didShutdown();
     }
 
     foreach ($plugins as $plugin) {
       $plugin->setData($plugin->generateData());
     }
 
     $plugins = msort($plugins, 'getOrderKey');
 
     $key = Filesystem::readRandomCharacters(24);
 
     $tabs = array();
     $data = array();
     foreach ($plugins as $plugin) {
       $class = get_class($plugin);
       $tabs[] = array(
         'class' => $class,
         'name'  => $plugin->getName(),
         'color' => $plugin->getColor(),
       );
       $data[$class] = $this->sanitizeForJSON($plugin->getData());
     }
 
     $storage = array(
       'vers' => self::STORAGE_VERSION,
       'tabs' => $tabs,
       'data' => $data,
       'user' => $request->getUser()
         ? $request->getUser()->getPHID()
         : null,
     );
 
     $cache = new PhabricatorKeyValueDatabaseCache();
     $cache = new PhutilKeyValueCacheProfiler($cache);
     $cache->setProfiler(PhutilServiceProfiler::getInstance());
 
     // This encoding may fail if there are, e.g., database queries which
     // include binary data. It would be a little cleaner to try to strip these,
     // but just do something non-broken here if we end up with unrepresentable
     // data.
     $json = @json_encode($storage);
     if (!$json) {
       $json = '{}';
     }
 
     $cache->setKeys(
       array(
         'darkconsole:'.$key => $json,
       ),
       $ttl = (60 * 60 * 6));
 
     return $key;
   }
 
   public function getColor() {
     foreach ($this->getPlugins() as $plugin) {
       if ($plugin->getColor()) {
         return $plugin->getColor();
       }
     }
   }
 
   public function render(AphrontRequest $request) {
     $user = $request->getUser();
     $visible = $user->getUserSetting(
       PhabricatorDarkConsoleVisibleSetting::SETTINGKEY);
 
     return javelin_tag(
       'div',
       array(
         'id' => 'darkconsole',
         'class' => 'dark-console',
         'style' => $visible ? '' : 'display: none;',
         'data-console-key' => $this->getKey($request),
         'data-console-color' => $this->getColor(),
       ),
       '');
   }
 
   /**
    * Sometimes, tab data includes binary information (like INSERT queries which
    * write file data into the database). To successfully JSON encode it, we
    * need to convert it to UTF-8.
    */
   private function sanitizeForJSON($data) {
     if (is_object($data)) {
       return '<object:'.get_class($data).'>';
     } else if (is_array($data)) {
       foreach ($data as $key => $value) {
         $data[$key] = $this->sanitizeForJSON($value);
       }
       return $data;
+    } else if (is_resource($data)) {
+      return '<resource>';
     } else {
       // Truncate huge strings. Since the data doesn't really matter much,
       // just truncate bytes to avoid PhutilUTF8StringTruncator overhead.
       $length = strlen($data);
       $max = 4096;
       if ($length > $max) {
         $data = substr($data, 0, $max).'...<'.$length.' bytes>...';
       }
       return phutil_utf8ize($data);
     }
   }
 
 }
diff --git a/src/applications/files/document/PhabricatorDocumentEngine.php b/src/applications/files/document/PhabricatorDocumentEngine.php
index 9593e1d98d..3a13c5eb4a 100644
--- a/src/applications/files/document/PhabricatorDocumentEngine.php
+++ b/src/applications/files/document/PhabricatorDocumentEngine.php
@@ -1,284 +1,288 @@
 <?php
 
 abstract class PhabricatorDocumentEngine
   extends Phobject {
 
   private $viewer;
   private $highlightedLines = array();
   private $encodingConfiguration;
   private $highlightingConfiguration;
   private $blameConfiguration = true;
 
   final public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   final public function getViewer() {
     return $this->viewer;
   }
 
   final public function setHighlightedLines(array $highlighted_lines) {
     $this->highlightedLines = $highlighted_lines;
     return $this;
   }
 
   final public function getHighlightedLines() {
     return $this->highlightedLines;
   }
 
   final public function canRenderDocument(PhabricatorDocumentRef $ref) {
     return $this->canRenderDocumentType($ref);
   }
 
   public function canDiffDocuments(
     PhabricatorDocumentRef $uref = null,
     PhabricatorDocumentRef $vref = null) {
     return false;
   }
 
   public function newBlockDiffViews(
     PhabricatorDocumentRef $uref,
     PhabricatorDocumentEngineBlock $ublock,
     PhabricatorDocumentRef $vref,
     PhabricatorDocumentEngineBlock $vblock) {
 
     $u_content = $this->newBlockContentView($uref, $ublock);
     $v_content = $this->newBlockContentView($vref, $vblock);
 
     return id(new PhabricatorDocumentEngineBlockDiff())
       ->setOldContent($u_content)
       ->addOldClass('old')
       ->addOldClass('old-full')
       ->setNewContent($v_content)
       ->addNewClass('new')
       ->addNewClass('new-full');
   }
 
   public function newBlockContentView(
     PhabricatorDocumentRef $ref,
     PhabricatorDocumentEngineBlock $block) {
     return $block->getContent();
   }
 
   public function newEngineBlocks(
     PhabricatorDocumentRef $uref,
     PhabricatorDocumentRef $vref) {
     throw new PhutilMethodNotImplementedException();
   }
 
   public function canConfigureEncoding(PhabricatorDocumentRef $ref) {
     return false;
   }
 
   public function canConfigureHighlighting(PhabricatorDocumentRef $ref) {
     return false;
   }
 
   public function canBlame(PhabricatorDocumentRef $ref) {
     return false;
   }
 
   final public function setEncodingConfiguration($config) {
     $this->encodingConfiguration = $config;
     return $this;
   }
 
   final public function getEncodingConfiguration() {
     return $this->encodingConfiguration;
   }
 
   final public function setHighlightingConfiguration($config) {
     $this->highlightingConfiguration = $config;
     return $this;
   }
 
   final public function getHighlightingConfiguration() {
     return $this->highlightingConfiguration;
   }
 
   final public function setBlameConfiguration($blame_configuration) {
     $this->blameConfiguration = $blame_configuration;
     return $this;
   }
 
   final public function getBlameConfiguration() {
     return $this->blameConfiguration;
   }
 
   final protected function getBlameEnabled() {
     return $this->blameConfiguration;
   }
 
   public function shouldRenderAsync(PhabricatorDocumentRef $ref) {
     return false;
   }
 
   abstract protected function canRenderDocumentType(
     PhabricatorDocumentRef $ref);
 
   final public function newDocument(PhabricatorDocumentRef $ref) {
     $can_complete = $this->canRenderCompleteDocument($ref);
     $can_partial = $this->canRenderPartialDocument($ref);
 
     if (!$can_complete && !$can_partial) {
       return $this->newMessage(
         pht(
           'This document is too large to be rendered inline. (The document '.
           'is %s bytes, the limit for this engine is %s bytes.)',
           new PhutilNumber($ref->getByteLength()),
           new PhutilNumber($this->getByteLengthLimit())));
     }
 
     return $this->newDocumentContent($ref);
   }
 
   final public function newDocumentIcon(PhabricatorDocumentRef $ref) {
     return id(new PHUIIconView())
       ->setIcon($this->getDocumentIconIcon($ref));
   }
 
   abstract protected function newDocumentContent(
     PhabricatorDocumentRef $ref);
 
   protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
     return 'fa-file-o';
   }
 
   protected function getDocumentRenderingText(PhabricatorDocumentRef $ref) {
     return pht('Loading...');
   }
 
   final public function getDocumentEngineKey() {
     return $this->getPhobjectClassConstant('ENGINEKEY');
   }
 
   final public static function getAllEngines() {
     return id(new PhutilClassMapQuery())
       ->setAncestorClass(__CLASS__)
       ->setUniqueMethod('getDocumentEngineKey')
       ->execute();
   }
 
   final public function newSortVector(PhabricatorDocumentRef $ref) {
     $content_score = $this->getContentScore($ref);
 
     // Prefer engines which can render the entire file over engines which
     // can only render a header, and engines which can render a header over
     // engines which can't render anything.
     if ($this->canRenderCompleteDocument($ref)) {
       $limit_score = 0;
     } else if ($this->canRenderPartialDocument($ref)) {
       $limit_score = 1;
     } else {
       $limit_score = 2;
     }
 
     return id(new PhutilSortVector())
       ->addInt($limit_score)
       ->addInt(-$content_score);
   }
 
   protected function getContentScore(PhabricatorDocumentRef $ref) {
     return 2000;
   }
 
   abstract public function getViewAsLabel(PhabricatorDocumentRef $ref);
 
   public function getViewAsIconIcon(PhabricatorDocumentRef $ref) {
     $can_complete = $this->canRenderCompleteDocument($ref);
     $can_partial = $this->canRenderPartialDocument($ref);
 
     if (!$can_complete && !$can_partial) {
       return 'fa-times';
     }
 
     return $this->getDocumentIconIcon($ref);
   }
 
   public function getViewAsIconColor(PhabricatorDocumentRef $ref) {
     $can_complete = $this->canRenderCompleteDocument($ref);
 
     if (!$can_complete) {
       return 'grey';
     }
 
     return null;
   }
 
   final public static function getEnginesForRef(
     PhabricatorUser $viewer,
     PhabricatorDocumentRef $ref) {
     $engines = self::getAllEngines();
 
     foreach ($engines as $key => $engine) {
       $engine = id(clone $engine)
         ->setViewer($viewer);
 
       if (!$engine->canRenderDocument($ref)) {
         unset($engines[$key]);
         continue;
       }
 
       $engines[$key] = $engine;
     }
 
     if (!$engines) {
       throw new Exception(pht('No content engine can render this document.'));
     }
 
     $vectors = array();
     foreach ($engines as $key => $usable_engine) {
       $vectors[$key] = $usable_engine->newSortVector($ref);
     }
     $vectors = msortv($vectors, 'getSelf');
 
     return array_select_keys($engines, array_keys($vectors));
   }
 
   protected function getByteLengthLimit() {
     return (1024 * 1024 * 8);
   }
 
   protected function canRenderCompleteDocument(PhabricatorDocumentRef $ref) {
     $limit = $this->getByteLengthLimit();
     if ($limit) {
       $length = $ref->getByteLength();
       if ($length > $limit) {
         return false;
       }
     }
 
     return true;
   }
 
   protected function canRenderPartialDocument(PhabricatorDocumentRef $ref) {
     return false;
   }
 
   protected function newMessage($message) {
     return phutil_tag(
       'div',
       array(
         'class' => 'document-engine-error',
       ),
       $message);
   }
 
   final public function newLoadingContent(PhabricatorDocumentRef $ref) {
     $spinner = id(new PHUIIconView())
       ->setIcon('fa-gear')
       ->addClass('ph-spin');
 
     return phutil_tag(
       'div',
       array(
         'class' => 'document-engine-loading',
       ),
       array(
         $spinner,
         $this->getDocumentRenderingText($ref),
       ));
   }
 
+  public function shouldSuggestEngine(PhabricatorDocumentRef $ref) {
+    return false;
+  }
+
 }
diff --git a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php
index 834a880b42..3e479fdace 100644
--- a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php
+++ b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php
@@ -1,751 +1,755 @@
 <?php
 
 final class PhabricatorJupyterDocumentEngine
   extends PhabricatorDocumentEngine {
 
   const ENGINEKEY = 'jupyter';
 
   public function getViewAsLabel(PhabricatorDocumentRef $ref) {
     return pht('View as Jupyter Notebook');
   }
 
   protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
     return 'fa-sun-o';
   }
 
   protected function getDocumentRenderingText(PhabricatorDocumentRef $ref) {
     return pht('Rendering Jupyter Notebook...');
   }
 
   public function shouldRenderAsync(PhabricatorDocumentRef $ref) {
     return true;
   }
 
   protected function getContentScore(PhabricatorDocumentRef $ref) {
     $name = $ref->getName();
 
     if (preg_match('/\\.ipynb\z/i', $name)) {
       return 2000;
     }
 
     return 500;
   }
 
   protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
     return $ref->isProbablyJSON();
   }
 
   public function canDiffDocuments(
     PhabricatorDocumentRef $uref = null,
     PhabricatorDocumentRef $vref = null) {
     return true;
   }
 
   public function newEngineBlocks(
     PhabricatorDocumentRef $uref = null,
     PhabricatorDocumentRef $vref = null) {
 
     $blocks = new PhabricatorDocumentEngineBlocks();
 
     try {
       if ($uref) {
         $u_blocks = $this->newDiffBlocks($uref);
       } else {
         $u_blocks = array();
       }
 
       if ($vref) {
         $v_blocks = $this->newDiffBlocks($vref);
       } else {
         $v_blocks = array();
       }
 
       $blocks->addBlockList($uref, $u_blocks);
       $blocks->addBlockList($vref, $v_blocks);
     } catch (Exception $ex) {
       $blocks->addMessage($ex->getMessage());
     }
 
     return $blocks;
   }
 
   public function newBlockDiffViews(
     PhabricatorDocumentRef $uref,
     PhabricatorDocumentEngineBlock $ublock,
     PhabricatorDocumentRef $vref,
     PhabricatorDocumentEngineBlock $vblock) {
 
     $ucell = $ublock->getContent();
     $vcell = $vblock->getContent();
 
     $utype = idx($ucell, 'cell_type');
     $vtype = idx($vcell, 'cell_type');
 
     if ($utype === $vtype) {
       switch ($utype) {
         case 'markdown':
           $usource = idx($ucell, 'source');
           $usource = implode('', $usource);
 
           $vsource = idx($vcell, 'source');
           $vsource = implode('', $vsource);
 
           $diff = id(new PhutilProseDifferenceEngine())
             ->getDiff($usource, $vsource);
 
           $u_content = $this->newProseDiffCell($diff, array('=', '-'));
           $v_content = $this->newProseDiffCell($diff, array('=', '+'));
 
           $u_content = $this->newJupyterCell(null, $u_content, null);
           $v_content = $this->newJupyterCell(null, $v_content, null);
 
           $u_content = $this->newCellContainer($u_content);
           $v_content = $this->newCellContainer($v_content);
 
           return id(new PhabricatorDocumentEngineBlockDiff())
             ->setOldContent($u_content)
             ->addOldClass('old')
             ->setNewContent($v_content)
             ->addNewClass('new');
         case 'code/line':
           $usource = idx($ucell, 'raw');
           $vsource = idx($vcell, 'raw');
           $udisplay = idx($ucell, 'display');
           $vdisplay = idx($vcell, 'display');
           $ulabel = idx($ucell, 'label');
           $vlabel = idx($vcell, 'label');
 
           $intraline_segments = ArcanistDiffUtils::generateIntralineDiff(
             $usource,
             $vsource);
 
           $u_segments = array();
           foreach ($intraline_segments[0] as $u_segment) {
             $u_segments[] = $u_segment;
           }
 
           $v_segments = array();
           foreach ($intraline_segments[1] as $v_segment) {
             $v_segments[] = $v_segment;
           }
 
           $usource = PhabricatorDifferenceEngine::applyIntralineDiff(
             $udisplay,
             $u_segments);
 
           $vsource = PhabricatorDifferenceEngine::applyIntralineDiff(
             $vdisplay,
             $v_segments);
 
           $u_content = $this->newCodeLineCell($ucell, $usource);
           $v_content = $this->newCodeLineCell($vcell, $vsource);
 
           $classes = array(
             'jupyter-cell-flush',
           );
 
           $u_content = $this->newJupyterCell($ulabel, $u_content, $classes);
           $v_content = $this->newJupyterCell($vlabel, $v_content, $classes);
 
           $u_content = $this->newCellContainer($u_content);
           $v_content = $this->newCellContainer($v_content);
 
           return id(new PhabricatorDocumentEngineBlockDiff())
             ->setOldContent($u_content)
             ->addOldClass('old')
             ->setNewContent($v_content)
             ->addNewClass('new');
       }
     }
 
     return parent::newBlockDiffViews($uref, $ublock, $vref, $vblock);
   }
 
   public function newBlockContentView(
     PhabricatorDocumentRef $ref,
     PhabricatorDocumentEngineBlock $block) {
 
     $viewer = $this->getViewer();
     $cell = $block->getContent();
 
     $cell_content = $this->renderJupyterCell($viewer, $cell);
 
     return $this->newCellContainer($cell_content);
   }
 
   private function newCellContainer($cell_content) {
     $notebook_table = phutil_tag(
       'table',
       array(
         'class' => 'jupyter-notebook',
       ),
       $cell_content);
 
     $container = phutil_tag(
       'div',
       array(
         'class' => 'document-engine-jupyter document-engine-diff',
       ),
       $notebook_table);
 
     return $container;
   }
 
   private function newProseDiffCell(PhutilProseDiff $diff, array $mask) {
     $mask = array_fuse($mask);
 
     $result = array();
     foreach ($diff->getParts() as $part) {
       $type = $part['type'];
       $text = $part['text'];
 
       if (!isset($mask[$type])) {
         continue;
       }
 
       switch ($type) {
         case '-':
           $result[] = phutil_tag(
             'span',
             array(
               'class' => 'bright',
             ),
             $text);
           break;
         case '+':
           $result[] = phutil_tag(
             'span',
             array(
               'class' => 'bright',
             ),
             $text);
           break;
         case '=':
           $result[] = $text;
           break;
       }
     }
 
     return array(
       null,
       phutil_tag(
         'div',
         array(
           'class' => 'jupyter-cell-markdown',
         ),
         $result),
     );
   }
 
   private function newDiffBlocks(PhabricatorDocumentRef $ref) {
     $viewer = $this->getViewer();
     $content = $ref->loadData();
 
     $cells = $this->newCells($content, true);
 
     $idx = 1;
     $blocks = array();
     foreach ($cells as $cell) {
       // When the cell is a source code line, we can hash just the raw
       // input rather than all the cell metadata.
 
       switch (idx($cell, 'cell_type')) {
         case 'code/line':
           $hash_input = $cell['raw'];
           break;
         case 'markdown':
           $hash_input = implode('', $cell['source']);
           break;
         default:
           $hash_input = serialize($cell);
           break;
       }
 
       $hash = PhabricatorHash::digestWithNamedKey(
         $hash_input,
         'document-engine.content-digest');
 
       $blocks[] = id(new PhabricatorDocumentEngineBlock())
         ->setBlockKey($idx)
         ->setDifferenceHash($hash)
         ->setContent($cell);
 
       $idx++;
     }
 
     return $blocks;
   }
 
   protected function newDocumentContent(PhabricatorDocumentRef $ref) {
     $viewer = $this->getViewer();
     $content = $ref->loadData();
 
     try {
       $cells = $this->newCells($content, false);
     } catch (Exception $ex) {
       return $this->newMessage($ex->getMessage());
     }
 
     $rows = array();
     foreach ($cells as $cell) {
       $rows[] = $this->renderJupyterCell($viewer, $cell);
     }
 
     $notebook_table = phutil_tag(
       'table',
       array(
         'class' => 'jupyter-notebook',
       ),
       $rows);
 
     $container = phutil_tag(
       'div',
       array(
         'class' => 'document-engine-jupyter',
       ),
       $notebook_table);
 
     return $container;
   }
 
   private function newCells($content, $for_diff) {
     try {
       $data = phutil_json_decode($content);
     } catch (PhutilJSONParserException $ex) {
       throw new Exception(
         pht(
           'This is not a valid JSON document and can not be rendered as '.
           'a Jupyter notebook: %s.',
           $ex->getMessage()));
     }
 
     if (!is_array($data)) {
       throw new Exception(
         pht(
           'This document does not encode a valid JSON object and can not '.
           'be rendered as a Jupyter notebook.'));
     }
 
 
     $nbformat = idx($data, 'nbformat');
     if (!strlen($nbformat)) {
       throw new Exception(
         pht(
           'This document is missing an "nbformat" field. Jupyter notebooks '.
           'must have this field.'));
     }
 
     if ($nbformat !== 4) {
       throw new Exception(
         pht(
           'This Jupyter notebook uses an unsupported version of the file '.
           'format (found version %s, expected version 4).',
           $nbformat));
     }
 
     $cells = idx($data, 'cells');
     if (!is_array($cells)) {
       throw new Exception(
         pht(
           'This Jupyter notebook does not specify a list of "cells".'));
     }
 
     if (!$cells) {
       throw new Exception(
         pht(
           'This Jupyter notebook does not specify any notebook cells.'));
     }
 
     if (!$for_diff) {
       return $cells;
     }
 
     // If we're extracting cells to build a diff view, split code cells into
     // individual lines and individual outputs. We want users to be able to
     // add inline comments to each line and each output block.
 
     $results = array();
     foreach ($cells as $cell) {
       $cell_type = idx($cell, 'cell_type');
 
       if ($cell_type === 'markdown') {
         $source = $cell['source'];
         $source = implode('', $source);
 
         // Attempt to split contiguous blocks of markdown into smaller
         // pieces.
 
         $chunks = preg_split(
           '/\n\n+/',
           $source);
 
         foreach ($chunks as $chunk) {
           $result = $cell;
           $result['source'] = array($chunk);
           $results[] = $result;
         }
 
         continue;
       }
 
       if ($cell_type !== 'code') {
         $results[] = $cell;
         continue;
       }
 
       $label = $this->newCellLabel($cell);
 
       $lines = idx($cell, 'source');
       if (!is_array($lines)) {
         $lines = array();
       }
 
       $content = $this->highlightLines($lines);
 
       $count = count($lines);
       for ($ii = 0; $ii < $count; $ii++) {
         $is_head = ($ii === 0);
         $is_last = ($ii === ($count - 1));
 
         if ($is_head) {
           $line_label = $label;
         } else {
           $line_label = null;
         }
 
         $results[] = array(
           'cell_type' => 'code/line',
           'label' => $line_label,
           'raw' => $lines[$ii],
           'display' => idx($content, $ii),
           'head' => $is_head,
           'last' => $is_last,
         );
       }
 
       $outputs = array();
       $output_list = idx($cell, 'outputs');
       if (is_array($output_list)) {
         foreach ($output_list as $output) {
           $results[] = array(
             'cell_type' => 'code/output',
             'output' => $output,
           );
         }
       }
     }
 
     return $results;
   }
 
 
   private function renderJupyterCell(
     PhabricatorUser $viewer,
     array $cell) {
 
     list($label, $content) = $this->renderJupyterCellContent($viewer, $cell);
 
     $classes = null;
     switch (idx($cell, 'cell_type')) {
       case 'code/line':
         $classes = 'jupyter-cell-flush';
         break;
     }
 
     return $this->newJupyterCell(
       $label,
       $content,
       $classes);
   }
 
   private function newJupyterCell($label, $content, $classes) {
     $label_cell = phutil_tag(
       'td',
       array(
         'class' => 'jupyter-label',
       ),
       $label);
 
     $content_cell = phutil_tag(
       'td',
       array(
         'class' => $classes,
       ),
       $content);
 
     return phutil_tag(
       'tr',
       array(),
       array(
         $label_cell,
         $content_cell,
       ));
   }
 
   private function renderJupyterCellContent(
     PhabricatorUser $viewer,
     array $cell) {
 
     $cell_type = idx($cell, 'cell_type');
     switch ($cell_type) {
       case 'markdown':
         return $this->newMarkdownCell($cell);
       case 'code':
         return $this->newCodeCell($cell);
       case 'code/line':
         return $this->newCodeLineCell($cell);
       case 'code/output':
         return $this->newCodeOutputCell($cell);
     }
 
     $json_content = id(new PhutilJSON())
       ->encodeFormatted($cell);
 
     return $this->newRawCell($json_content);
   }
 
   private function newRawCell($content) {
     return array(
       null,
       phutil_tag(
         'div',
         array(
           'class' => 'jupyter-cell-raw PhabricatorMonospaced',
         ),
         $content),
     );
   }
 
   private function newMarkdownCell(array $cell) {
     $content = idx($cell, 'source');
     if (!is_array($content)) {
       $content = array();
     }
 
     // TODO: This should ideally highlight as Markdown, but the "md"
     // highlighter in Pygments is painfully slow and not terribly useful.
     $content = $this->highlightLines($content, 'txt');
 
     return array(
       null,
       phutil_tag(
         'div',
         array(
           'class' => 'jupyter-cell-markdown',
         ),
         $content),
     );
   }
 
   private function newCodeCell(array $cell) {
     $label = $this->newCellLabel($cell);
 
     $content = idx($cell, 'source');
     if (!is_array($content)) {
       $content = array();
     }
 
     $content = $this->highlightLines($content);
 
     $outputs = array();
     $output_list = idx($cell, 'outputs');
     if (is_array($output_list)) {
       foreach ($output_list as $output) {
         $outputs[] = $this->newOutput($output);
       }
     }
 
     return array(
       $label,
       array(
         phutil_tag(
           'div',
           array(
             'class' =>
               'jupyter-cell-code jupyter-cell-code-block '.
               'PhabricatorMonospaced remarkup-code',
           ),
           array(
             $content,
           )),
         $outputs,
       ),
     );
   }
 
   private function newCodeLineCell(array $cell, $content = null) {
     $classes = array();
     $classes[] = 'PhabricatorMonospaced';
     $classes[] = 'remarkup-code';
     $classes[] = 'jupyter-cell-code';
     $classes[] = 'jupyter-cell-code-line';
 
     if ($cell['head']) {
       $classes[] = 'jupyter-cell-code-head';
     }
 
     if ($cell['last']) {
       $classes[] = 'jupyter-cell-code-last';
     }
 
     $classes = implode(' ', $classes);
 
     if ($content === null) {
       $content = $cell['display'];
     }
 
     return array(
       $cell['label'],
       array(
         phutil_tag(
           'div',
           array(
             'class' => $classes,
           ),
           array(
             $content,
           )),
       ),
     );
   }
 
   private function newCodeOutputCell(array $cell) {
     return array(
       null,
       $this->newOutput($cell['output']),
     );
   }
 
   private function newOutput(array $output) {
     if (!is_array($output)) {
       return pht('<Invalid Output>');
     }
 
     $classes = array(
       'jupyter-output',
       'PhabricatorMonospaced',
     );
 
     $output_name = idx($output, 'name');
     switch ($output_name) {
       case 'stderr':
         $classes[] = 'jupyter-output-stderr';
         break;
     }
 
     $output_type = idx($output, 'output_type');
     switch ($output_type) {
       case 'execute_result':
       case 'display_data':
         $data = idx($output, 'data');
 
         $image_formats = array(
           'image/png',
           'image/jpeg',
           'image/jpg',
           'image/gif',
         );
 
         foreach ($image_formats as $image_format) {
           if (!isset($data[$image_format])) {
             continue;
           }
 
           $raw_data = $data[$image_format];
           if (!is_array($raw_data)) {
             $raw_data = array($raw_data);
           }
           $raw_data = implode('', $raw_data);
 
           $content = phutil_tag(
             'img',
             array(
               'src' => 'data:'.$image_format.';base64,'.$raw_data,
             ));
 
           break 2;
         }
 
         if (isset($data['text/html'])) {
           $content = $data['text/html'];
           $classes[] = 'jupyter-output-html';
           break;
         }
 
         if (isset($data['application/javascript'])) {
           $content = $data['application/javascript'];
           $classes[] = 'jupyter-output-html';
           break;
         }
 
         if (isset($data['text/plain'])) {
           $content = $data['text/plain'];
           break;
         }
 
         break;
       case 'stream':
       default:
         $content = idx($output, 'text');
         if (!is_array($content)) {
           $content = array();
         }
         $content = implode('', $content);
         break;
     }
 
     return phutil_tag(
       'div',
       array(
         'class' => implode(' ', $classes),
       ),
       $content);
   }
 
   private function newCellLabel(array $cell) {
     $execution_count = idx($cell, 'execution_count');
     if ($execution_count) {
       $label = 'In ['.$execution_count.']:';
     } else {
       $label = null;
     }
 
     return $label;
   }
 
   private function highlightLines(array $lines, $force_language = null) {
     if ($force_language === null) {
       $head = head($lines);
       $matches = null;
       if (preg_match('/^%%(.*)$/', $head, $matches)) {
         $restore = array_shift($lines);
         $lang = $matches[1];
       } else {
         $restore = null;
         $lang = 'py';
       }
     } else {
       $restore = null;
       $lang = $force_language;
     }
 
     $content = PhabricatorSyntaxHighlighter::highlightWithLanguage(
       $lang,
       implode('', $lines));
     $content = phutil_split_lines($content);
 
     if ($restore !== null) {
       $language_tag = phutil_tag(
         'span',
         array(
           'class' => 'language-tag',
         ),
         $restore);
 
       array_unshift($content, $language_tag);
     }
 
     return $content;
   }
 
+  public function shouldSuggestEngine(PhabricatorDocumentRef $ref) {
+    return true;
+  }
+
 }
diff --git a/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php b/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php
index 2524f77821..5a4cb77340 100644
--- a/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php
+++ b/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php
@@ -1,338 +1,344 @@
 <?php
 
 abstract class PhabricatorDocumentRenderingEngine
   extends Phobject {
 
   private $request;
   private $controller;
   private $activeEngine;
   private $ref;
 
   final public function setRequest(AphrontRequest $request) {
     $this->request = $request;
     return $this;
   }
 
   final public function getRequest() {
     if (!$this->request) {
       throw new PhutilInvalidStateException('setRequest');
     }
 
     return $this->request;
   }
 
   final public function setController(PhabricatorController $controller) {
     $this->controller = $controller;
     return $this;
   }
 
   final public function getController() {
     if (!$this->controller) {
       throw new PhutilInvalidStateException('setController');
     }
 
     return $this->controller;
   }
 
   final protected function getActiveEngine() {
     return $this->activeEngine;
   }
 
   final protected function getRef() {
     return $this->ref;
   }
 
   final public function newDocumentView(PhabricatorDocumentRef $ref) {
     $request = $this->getRequest();
     $viewer = $request->getViewer();
 
     $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref);
 
     $engine_key = $this->getSelectedDocumentEngineKey();
     if (!isset($engines[$engine_key])) {
       $engine_key = head_key($engines);
     }
     $engine = $engines[$engine_key];
 
     $lines = $this->getSelectedLineRange();
     if ($lines) {
       $engine->setHighlightedLines(range($lines[0], $lines[1]));
     }
 
     $encode_setting = $request->getStr('encode');
     if (strlen($encode_setting)) {
       $engine->setEncodingConfiguration($encode_setting);
     }
 
     $highlight_setting = $request->getStr('highlight');
     if (strlen($highlight_setting)) {
       $engine->setHighlightingConfiguration($highlight_setting);
     }
 
     $blame_setting = ($request->getStr('blame') !== 'off');
     $engine->setBlameConfiguration($blame_setting);
 
     $views = array();
     foreach ($engines as $candidate_key => $candidate_engine) {
       $label = $candidate_engine->getViewAsLabel($ref);
       if ($label === null) {
         continue;
       }
 
       $view_uri = $this->newRefViewURI($ref, $candidate_engine);
 
       $view_icon = $candidate_engine->getViewAsIconIcon($ref);
       $view_color = $candidate_engine->getViewAsIconColor($ref);
       $loading = $candidate_engine->newLoadingContent($ref);
 
       $views[] = array(
         'viewKey' => $candidate_engine->getDocumentEngineKey(),
         'icon' => $view_icon,
         'color' => $view_color,
         'name' => $label,
         'engineURI' => $this->newRefRenderURI($ref, $candidate_engine),
         'viewURI' => $view_uri,
         'loadingMarkup' => hsprintf('%s', $loading),
         'canEncode' => $candidate_engine->canConfigureEncoding($ref),
         'canHighlight' => $candidate_engine->canConfigureHighlighting($ref),
         'canBlame' => $candidate_engine->canBlame($ref),
       );
     }
 
     $viewport_id = celerity_generate_unique_node_id();
     $control_id = celerity_generate_unique_node_id();
     $icon = $engine->newDocumentIcon($ref);
 
     $config = array(
       'controlID' => $control_id,
     );
 
     $this->willStageRef($ref);
 
     if ($engine->shouldRenderAsync($ref)) {
       $content = $engine->newLoadingContent($ref);
       $config['next'] = 'render';
     } else {
       $this->willRenderRef($ref);
       $content = $engine->newDocument($ref);
 
       if ($engine->canBlame($ref)) {
         $config['next'] = 'blame';
       }
     }
 
     Javelin::initBehavior('document-engine', $config);
 
     $viewport = phutil_tag(
       'div',
       array(
         'id' => $viewport_id,
       ),
       $content);
 
     $meta = array(
       'viewportID' => $viewport_id,
       'viewKey' => $engine->getDocumentEngineKey(),
       'views' => $views,
       'encode' => array(
         'icon' => 'fa-font',
         'name' => pht('Change Text Encoding...'),
         'uri' => '/services/encoding/',
         'value' => $encode_setting,
       ),
       'highlight' => array(
         'icon' => 'fa-lightbulb-o',
         'name' => pht('Highlight As...'),
         'uri' => '/services/highlight/',
         'value' => $highlight_setting,
       ),
       'blame' => array(
         'icon' => 'fa-backward',
         'hide' => pht('Hide Blame'),
         'show' => pht('Show Blame'),
         'uri' => $ref->getBlameURI(),
         'enabled' => $blame_setting,
         'value' => null,
       ),
       'coverage' => array(
         'labels' => array(
           // TODO: Modularize this properly, see T13125.
           array(
             'C' => pht('Covered'),
             'U' => pht('Not Covered'),
             'N' => pht('Not Executable'),
             'X' => pht('Not Reachable'),
           ),
         ),
       ),
     );
 
     $view_button = id(new PHUIButtonView())
       ->setTag('a')
       ->setText(pht('View Options'))
       ->setIcon('fa-file-image-o')
       ->setColor(PHUIButtonView::GREY)
       ->setID($control_id)
       ->setMetadata($meta)
       ->setDropdown(true)
       ->addSigil('document-engine-view-dropdown');
 
     $header = id(new PHUIHeaderView())
       ->setHeaderIcon($icon)
       ->setHeader($ref->getName())
       ->addActionLink($view_button);
 
     return id(new PHUIObjectBoxView())
       ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
       ->setHeader($header)
       ->appendChild($viewport);
   }
 
   final public function newRenderResponse(PhabricatorDocumentRef $ref) {
     $this->willStageRef($ref);
     $this->willRenderRef($ref);
 
     $request = $this->getRequest();
     $viewer = $request->getViewer();
 
     $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref);
     $engine_key = $this->getSelectedDocumentEngineKey();
     if (!isset($engines[$engine_key])) {
       return $this->newErrorResponse(
         pht(
           'The engine ("%s") is unknown, or unable to render this document.',
           $engine_key));
     }
     $engine = $engines[$engine_key];
 
     $this->activeEngine = $engine;
 
     $encode_setting = $request->getStr('encode');
     if (strlen($encode_setting)) {
       $engine->setEncodingConfiguration($encode_setting);
     }
 
     $highlight_setting = $request->getStr('highlight');
     if (strlen($highlight_setting)) {
       $engine->setHighlightingConfiguration($highlight_setting);
     }
 
     $blame_setting = ($request->getStr('blame') !== 'off');
     $engine->setBlameConfiguration($blame_setting);
 
     try {
       $content = $engine->newDocument($ref);
     } catch (Exception $ex) {
       return $this->newErrorResponse($ex->getMessage());
     }
 
     return $this->newContentResponse($content);
   }
 
   public function newErrorResponse($message) {
     $container = phutil_tag(
       'div',
       array(
         'class' => 'document-engine-error',
       ),
       array(
         id(new PHUIIconView())
           ->setIcon('fa-exclamation-triangle red'),
         ' ',
         $message,
       ));
 
     return $this->newContentResponse($container);
   }
 
   private function newContentResponse($content) {
     $request = $this->getRequest();
     $viewer = $request->getViewer();
     $controller = $this->getController();
 
     if ($request->isAjax()) {
       return id(new AphrontAjaxResponse())
         ->setContent(
           array(
             'markup' => hsprintf('%s', $content),
           ));
     }
 
     $crumbs = $this->newCrumbs();
     $crumbs->setBorder(true);
 
     $content_frame = id(new PHUIObjectBoxView())
       ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
       ->appendChild($content);
 
     $page_frame = id(new PHUITwoColumnView())
       ->setFooter($content_frame);
 
     $title = array();
     $ref = $this->getRef();
     if ($ref) {
       $title = array(
         $ref->getName(),
         pht('Standalone'),
       );
     } else {
       $title = pht('Document');
     }
 
     return $controller->newPage()
       ->setCrumbs($crumbs)
       ->setTitle($title)
       ->appendChild($page_frame);
   }
 
   protected function newCrumbs() {
     $engine = $this->getActiveEngine();
     $controller = $this->getController();
 
     $crumbs = $controller->buildApplicationCrumbsForEditEngine();
 
     $ref = $this->getRef();
 
     $this->addApplicationCrumbs($crumbs, $ref);
 
     if ($ref) {
       $label = $engine->getViewAsLabel($ref);
       if ($label) {
         $crumbs->addTextCrumb($label);
       }
     }
 
     return $crumbs;
   }
 
+  public function getRefViewURI(
+    PhabricatorDocumentRef $ref,
+    PhabricatorDocumentEngine $engine) {
+    return $this->newRefViewURI($ref, $engine);
+  }
+
   abstract protected function newRefViewURI(
     PhabricatorDocumentRef $ref,
     PhabricatorDocumentEngine $engine);
 
   abstract protected function newRefRenderURI(
     PhabricatorDocumentRef $ref,
     PhabricatorDocumentEngine $engine);
 
   protected function getSelectedDocumentEngineKey() {
     return $this->getRequest()->getURIData('engineKey');
   }
 
   protected function getSelectedLineRange() {
     return $this->getRequest()->getURILineRange('lines', 1000);
   }
 
   protected function addApplicationCrumbs(
     PHUICrumbsView $crumbs,
     PhabricatorDocumentRef $ref = null) {
     return;
   }
 
   protected function willStageRef(PhabricatorDocumentRef $ref) {
     return;
   }
 
   protected function willRenderRef(PhabricatorDocumentRef $ref) {
     return;
   }
 
 }
diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php
index 9b4079fd46..b5736b784e 100644
--- a/src/applications/paste/controller/PhabricatorPasteViewController.php
+++ b/src/applications/paste/controller/PhabricatorPasteViewController.php
@@ -1,173 +1,230 @@
 <?php
 
 final class PhabricatorPasteViewController extends PhabricatorPasteController {
 
   public function shouldAllowPublic() {
     return true;
   }
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $request->getViewer();
     $id = $request->getURIData('id');
 
     $paste = id(new PhabricatorPasteQuery())
       ->setViewer($viewer)
       ->withIDs(array($id))
       ->needContent(true)
       ->needRawContent(true)
       ->executeOne();
     if (!$paste) {
       return new Aphront404Response();
     }
 
     $lines = $request->getURILineRange('lines', 1000);
     if ($lines) {
       $map = range($lines[0], $lines[1]);
     } else {
       $map = array();
     }
 
     $header = $this->buildHeaderView($paste);
     $curtain = $this->buildCurtain($paste);
 
     $subheader = $this->buildSubheaderView($paste);
     $source_code = $this->buildSourceCodeView($paste, $map);
 
     require_celerity_resource('paste-css');
 
     $monogram = $paste->getMonogram();
     $crumbs = $this->buildApplicationCrumbs()
       ->addTextCrumb($monogram)
       ->setBorder(true);
 
     $timeline = $this->buildTransactionTimeline(
       $paste,
       new PhabricatorPasteTransactionQuery());
 
     $comment_view = id(new PhabricatorPasteEditEngine())
       ->setViewer($viewer)
       ->buildEditEngineCommentView($paste);
 
     $timeline->setQuoteRef($monogram);
     $comment_view->setTransactionTimeline($timeline);
 
+    $recommendation_view = $this->newDocumentRecommendationView($paste);
+
     $paste_view = id(new PHUITwoColumnView())
       ->setHeader($header)
       ->setSubheader($subheader)
-      ->setMainColumn(array(
+      ->setMainColumn(
+        array(
+          $recommendation_view,
           $source_code,
           $timeline,
           $comment_view,
         ))
-      ->setCurtain($curtain)
-      ->addClass('ponder-question-view');
+      ->setCurtain($curtain);
 
     return $this->newPage()
       ->setTitle($paste->getFullName())
       ->setCrumbs($crumbs)
       ->setPageObjectPHIDs(
         array(
           $paste->getPHID(),
         ))
       ->appendChild($paste_view);
   }
 
   private function buildHeaderView(PhabricatorPaste $paste) {
     $title = (nonempty($paste->getTitle())) ?
       $paste->getTitle() : pht('(An Untitled Masterwork)');
 
     if ($paste->isArchived()) {
       $header_icon = 'fa-ban';
       $header_name = pht('Archived');
       $header_color = 'dark';
     } else {
       $header_icon = 'fa-check';
       $header_name = pht('Active');
       $header_color = 'bluegrey';
     }
 
     $header = id(new PHUIHeaderView())
       ->setHeader($title)
       ->setUser($this->getRequest()->getUser())
       ->setStatus($header_icon, $header_color, $header_name)
       ->setPolicyObject($paste)
       ->setHeaderIcon('fa-clipboard');
 
     return $header;
   }
 
   private function buildCurtain(PhabricatorPaste $paste) {
     $viewer = $this->getViewer();
     $curtain = $this->newCurtainView($paste);
 
     $can_edit = PhabricatorPolicyFilter::hasCapability(
       $viewer,
       $paste,
       PhabricatorPolicyCapability::CAN_EDIT);
 
     $id = $paste->getID();
     $edit_uri = $this->getApplicationURI("edit/{$id}/");
     $archive_uri = $this->getApplicationURI("archive/{$id}/");
     $raw_uri = $this->getApplicationURI("raw/{$id}/");
 
     $curtain->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('Edit Paste'))
         ->setIcon('fa-pencil')
         ->setDisabled(!$can_edit)
         ->setHref($edit_uri));
 
     if ($paste->isArchived()) {
       $curtain->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Activate Paste'))
           ->setIcon('fa-check')
           ->setDisabled(!$can_edit)
           ->setWorkflow($can_edit)
           ->setHref($archive_uri));
     } else {
       $curtain->addAction(
         id(new PhabricatorActionView())
           ->setName(pht('Archive Paste'))
           ->setIcon('fa-ban')
           ->setDisabled(!$can_edit)
           ->setWorkflow($can_edit)
           ->setHref($archive_uri));
     }
 
     $curtain->addAction(
       id(new PhabricatorActionView())
         ->setName(pht('View Raw File'))
         ->setIcon('fa-file-text-o')
         ->setHref($raw_uri));
 
     return $curtain;
   }
 
 
   private function buildSubheaderView(
     PhabricatorPaste $paste) {
     $viewer = $this->getViewer();
 
     $author = $viewer->renderHandle($paste->getAuthorPHID())->render();
     $date = phabricator_datetime($paste->getDateCreated(), $viewer);
     $author = phutil_tag('strong', array(), $author);
 
     $author_info = id(new PhabricatorPeopleQuery())
       ->setViewer($viewer)
       ->withPHIDs(array($paste->getAuthorPHID()))
       ->needProfileImage(true)
       ->executeOne();
 
     $image_uri = $author_info->getProfileImageURI();
     $image_href = '/p/'.$author_info->getUsername();
 
     $content = pht('Authored by %s on %s.', $author, $date);
 
     return id(new PHUIHeadThingView())
       ->setImage($image_uri)
       ->setImageHref($image_href)
       ->setContent($content);
   }
 
+  private function newDocumentRecommendationView(PhabricatorPaste $paste) {
+    $viewer = $this->getViewer();
+
+    // See PHI1703. If a viewer is looking at a document in Paste which has
+    // a good rendering via a DocumentEngine, suggest they view the content
+    // in Files instead so they can see it rendered.
+
+    $ref = id(new PhabricatorDocumentRef())
+      ->setName($paste->getTitle())
+      ->setData($paste->getRawContent());
+
+    $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref);
+    if (!$engines) {
+      return null;
+    }
+
+    $engine = head($engines);
+    if (!$engine->shouldSuggestEngine($ref)) {
+      return null;
+    }
+
+    $file = id(new PhabricatorFileQuery())
+      ->setViewer($viewer)
+      ->withPHIDs(array($paste->getFilePHID()))
+      ->executeOne();
+    if (!$file) {
+      return null;
+    }
+
+    $file_ref = id(new PhabricatorDocumentRef())
+      ->setFile($file);
+
+    $view_uri = id(new PhabricatorFileDocumentRenderingEngine())
+      ->getRefViewURI($file_ref, $engine);
+
+    $view_as_label = $engine->getViewAsLabel($file_ref);
+
+    $view_as_hint = pht(
+      'This content can be rendered as a document in Files.');
+
+    return id(new PHUIInfoView())
+      ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
+      ->addButton(
+        id(new PHUIButtonView())
+          ->setTag('a')
+          ->setText($view_as_label)
+          ->setHref($view_uri)
+          ->setColor('grey'))
+      ->setErrors(
+        array(
+          $view_as_hint,
+        ));
+  }
+
 }