diff --git a/src/applications/differential/view/DifferentialInlineCommentView.php b/src/applications/differential/view/DifferentialInlineCommentView.php
index 2a0ed077c..a52578a59 100644
--- a/src/applications/differential/view/DifferentialInlineCommentView.php
+++ b/src/applications/differential/view/DifferentialInlineCommentView.php
@@ -1,276 +1,287 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 final class DifferentialInlineCommentView extends AphrontView {
 
   private $inlineComment;
   private $onRight;
   private $buildScaffolding;
   private $handles;
   private $markupEngine;
   private $editable;
   private $preview;
   private $allowReply;
 
   public function setInlineComment(PhabricatorInlineCommentInterface $comment) {
     $this->inlineComment = $comment;
     return $this;
   }
 
   public function setOnRight($on_right) {
     $this->onRight = $on_right;
     return $this;
   }
 
   public function setBuildScaffolding($scaffold) {
     $this->buildScaffolding = $scaffold;
     return $this;
   }
 
   public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   public function setMarkupEngine(PhutilMarkupEngine $engine) {
     $this->markupEngine = $engine;
     return $this;
   }
 
   public function setEditable($editable) {
     $this->editable = $editable;
     return $this;
   }
 
   public function setPreview($preview) {
     $this->preview = $preview;
     return $this;
   }
 
   public function setAllowReply($allow_reply) {
     $this->allowReply = $allow_reply;
     return $this;
   }
 
   public function render() {
 
     $inline = $this->inlineComment;
 
     $start = $inline->getLineNumber();
     $length = $inline->getLineLength();
     if ($length) {
       $end = $start + $length;
       $line = 'Lines '.number_format($start).'-'.number_format($end);
     } else {
       $line = 'Line '.number_format($start);
     }
 
     $metadata = array(
       'id' => $inline->getID(),
       'number' => $inline->getLineNumber(),
       'length' => $inline->getLineLength(),
       'on_right' => $this->onRight,
       'original' => $inline->getContent(),
     );
 
     $sigil = 'differential-inline-comment';
+    if ($this->preview) {
+      $sigil = $sigil . ' differential-inline-comment-preview';
+    }
 
     $content = $inline->getContent();
     $handles = $this->handles;
 
     $links = array();
 
     $is_synthetic = false;
     if ($inline->getSyntheticAuthor()) {
       $is_synthetic = true;
     }
 
     $is_draft = false;
     if ($inline->isDraft() && !$is_synthetic) {
       $links[] = 'Not Submitted Yet';
       $is_draft = true;
     }
 
     if (!$this->preview) {
       $links[] = javelin_render_tag(
         'a',
         array(
           'href'  => '#',
           'mustcapture' => true,
           'sigil' => 'differential-inline-prev',
         ),
         'Previous');
 
       $links[] = javelin_render_tag(
         'a',
         array(
           'href'  => '#',
           'mustcapture' => true,
           'sigil' => 'differential-inline-next',
         ),
         'Next');
 
       if ($this->allowReply) {
 
         if (!$is_synthetic) {
 
           // NOTE: No product reason why you can't reply to these, but the reply
           // mechanism currently sends the inline comment ID to the server, not
           // file/line information, and synthetic comments don't have an inline
           // comment ID.
 
           $links[] = javelin_render_tag(
             'a',
             array(
               'href'        => '#',
               'mustcapture' => true,
               'sigil'       => 'differential-inline-reply',
             ),
             'Reply');
         }
 
       }
     }
 
     $anchor_name = 'inline-'.$inline->getID();
 
     if ($this->editable && !$this->preview) {
       $links[] = javelin_render_tag(
         'a',
         array(
           'href'        => '#',
           'mustcapture' => true,
           'sigil'       => 'differential-inline-edit',
         ),
         'Edit');
       $links[] = javelin_render_tag(
         'a',
         array(
           'href'        => '#',
           'mustcapture' => true,
           'sigil'       => 'differential-inline-delete',
         ),
         'Delete');
     } else if ($this->preview) {
       $links[] = javelin_render_tag(
         'a',
         array(
           'meta'        => array(
             'anchor' => $anchor_name,
           ),
           'sigil'       => 'differential-inline-preview-jump',
         ),
         'Not Visible');
+      $links[] = javelin_render_tag(
+        'a',
+        array(
+          'href'        => '#',
+          'mustcapture' => true,
+          'sigil'       => 'differential-inline-delete',
+        ),
+        'Delete');
     }
 
     if ($links) {
       $links =
         '<span class="differential-inline-comment-links">'.
           implode(' &middot; ', $links).
         '</span>';
     } else {
       $links = null;
     }
 
     $cache = $inline->getCache();
     if (strlen($cache)) {
       $content = $cache;
     } else {
       $content = $this->markupEngine->markupText($content);
       if ($inline->getID()) {
         $inline->setCache($content);
 
         $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
         $inline->save();
         unset($unguarded);
       }
     }
 
     if ($this->preview) {
       $anchor = null;
     } else {
       $anchor = phutil_render_tag(
         'a',
         array(
           'name'    => $anchor_name,
           'id'      => $anchor_name,
           'class'   => 'differential-inline-comment-anchor',
         ),
         '');
     }
 
     $classes = array(
       'differential-inline-comment',
     );
     if ($is_draft) {
       $classes[] = 'differential-inline-comment-unsaved-draft';
     }
     if ($is_synthetic) {
       $classes[] = 'differential-inline-comment-synthetic';
     }
     $classes = implode(' ', $classes);
 
     if ($is_synthetic) {
       $author = $inline->getSyntheticAuthor();
     } else {
       $author = $handles[$inline->getAuthorPHID()]->getName();
     }
 
     $markup = javelin_render_tag(
       'div',
       array(
         'class' => $classes,
         'sigil' => $sigil,
         'meta'  => $metadata,
       ),
       '<div class="differential-inline-comment-head">'.
         $anchor.
         $links.
         ' <span class="differential-inline-comment-line">'.$line.'</span> '.
         phutil_escape_html($author).
       '</div>'.
       '<div class="differential-inline-comment-content">'.
         '<div class="phabricator-remarkup">'.
           $content.
         '</div>'.
       '</div>');
 
     return $this->scaffoldMarkup($markup);
   }
 
   private function scaffoldMarkup($markup) {
     if (!$this->buildScaffolding) {
       return $markup;
     }
 
     $left_markup = !$this->onRight ? $markup : '';
     $right_markup = $this->onRight ? $markup : '';
 
     return
       '<table>'.
         '<tr class="inline">'.
           '<th></th>'.
           '<td>'.$left_markup.'</td>'.
           '<th></th>'.
           '<td colspan="2">'.$right_markup.'</td>'.
         '</tr>'.
       '</table>';
   }
 
 }
diff --git a/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js b/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
index eddd65531..6e544fe78 100644
--- a/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
+++ b/webroot/rsrc/js/application/differential/DifferentialInlineCommentEditor.js
@@ -1,266 +1,271 @@
 /**
  * @provides differential-inline-comment-editor
  * @requires javelin-dom
  *           javelin-util
  *           javelin-stratcom
  *           javelin-install
  *           javelin-request
  *           javelin-workflow
  */
 
 JX.install('DifferentialInlineCommentEditor', {
 
   construct : function(uri) {
     this._uri = uri;
   },
 
   events : ['done'],
 
   members : {
     _uri : null,
     _undoText : null,
     _skipOverInlineCommentRows : function(node) {
       // TODO: Move this semantic information out of class names.
       while (node && node.className.indexOf('inline') !== -1) {
         node = node.nextSibling;
       }
       return node;
     },
     _buildRequestData : function() {
       return {
         op : this.getOperation(),
         on_right : this.getOnRight(),
         id : this.getID(),
         number : this.getLineNumber(),
         is_new : this.getIsNew(),
         length : this.getLength(),
         changeset : this.getChangeset(),
         text : this.getText() || ''
       };
     },
     _draw : function(content, exact_row) {
       var row = this.getRow();
       var table = this.getTable();
       var target = exact_row ? row : this._skipOverInlineCommentRows(row);
 
       return copyRows(table, content, target);
     },
     _removeUndoLink : function() {
       var rows = JX.DifferentialInlineCommentEditor._undoRows;
       if (rows) {
         for (var ii = 0; ii < rows.length; ii++) {
           JX.DOM.remove(rows[ii]);
         }
       }
     },
     _undo : function() {
       this._removeUndoLink();
 
       this.setText(this._undoText);
       this.start();
     },
     _registerUndoListener : function() {
       if (!JX.DifferentialInlineCommentEditor._activeEditor) {
         JX.Stratcom.listen(
           'click',
           'differential-inline-comment-undo',
           function(e) {
             JX.DifferentialInlineCommentEditor._activeEditor._undo();
             e.kill();
           });
       }
       JX.DifferentialInlineCommentEditor._activeEditor = this;
     },
     _setRowState : function(state) {
       var is_hidden   = (state == 'hidden');
       var is_loading  = (state == 'loading');
       var row = this.getRow();
       JX.DOM.alterClass(row, 'differential-inline-hidden', is_hidden);
       JX.DOM.alterClass(row, 'differential-inline-loading', is_loading);
     },
     _didContinueWorkflow : function(response) {
       var drawn = this._draw(JX.$N('div', JX.$H(response)));
 
       var op = this.getOperation();
       if (op == 'edit') {
         this._setRowState('hidden');
       }
 
       JX.DOM.find(
         drawn[0],
         'textarea',
         'differential-inline-comment-edit-textarea').focus();
 
       var oncancel = JX.bind(this, function(e) {
         e.kill();
 
         this._didCancelWorkflow();
 
         if (op == 'edit') {
           this._setRowState('visible');
         }
 
         JX.DOM.remove(drawn[0]);
       });
       JX.DOM.listen(drawn[0], 'click', 'inline-edit-cancel', oncancel);
 
       var onsubmit = JX.bind(this, function(e) {
         e.kill();
 
         JX.Workflow.newFromForm(e.getTarget())
           .setHandler(JX.bind(this, function(response) {
             JX.DOM.remove(drawn[0]);
             if (op == 'edit') {
               this._setRowState('visible');
             }
             this._didCompleteWorkflow(response);
           }))
           .start();
 
         JX.DOM.alterClass(drawn[0], 'differential-inline-loading', true);
       });
       JX.DOM.listen(drawn[0], 'submit', 'inline-edit-form', onsubmit);
     },
     _didCompleteWorkflow : function(response) {
       var op = this.getOperation();
 
       // We don't get any markup back if the user deletes a comment, or saves
       // an empty comment (which effects a delete).
       if (response.markup) {
         this._draw(JX.$N('div', JX.$H(response.markup)));
       }
 
       // These operations remove the old row (edit adds a new row first).
       var remove_old = (op == 'edit' || op == 'delete');
       if (remove_old) {
         JX.DOM.remove(this.getRow());
+        var other_rows = this.getOtherRows();
+        for(var i = 0; i < other_rows.length; ++i) {
+          JX.DOM.remove(other_rows[i]);
+        }
       }
 
       // Once the user saves something, get rid of the 'undo' option. A
       // particular case where we need this is saving a delete, when we might
       // otherwise leave around an 'undo' for an earlier edit to the same
       // comment.
       this._removeUndoLink();
 
       JX.Stratcom.invoke('differential-inline-comment-update');
       this.invoke('done');
     },
     _didCancelWorkflow : function() {
       this.invoke('done');
 
       var op = this.getOperation();
       if (op == 'delete') {
         // No undo for delete, we prompt the user explicitly.
         return;
       }
 
       try {
         var textarea = JX.DOM.find(
           document.body, // TODO: use getDialogRootNode() when available
           'textarea',
           'differential-inline-comment-edit-textarea');
       } catch (ex) {
         // The close handler is called whenever the dialog closes, even if the
         // user closed it by completing the workflow with "Save". The
         // JX.Workflow API should probably be refined to allow programmatic
         // distinction of close caused by 'cancel' vs 'submit'. Testing for
         // presence of the textarea serves as a proxy for detecting a 'cancel'.
         return;
       }
 
       var text = textarea.value;
 
       // If the user hasn't edited the text (i.e., no change from original for
       // 'edit' or no text at all), don't offer them an undo.
       if (text == this.getOriginalText() || text == '') {
         return;
       }
 
       // Save the text so we can 'undo' back to it.
       this._undoText = text;
 
       var template = this.getOnRight()
         ? this.getTemplates().r
         : this.getTemplates().l;
       template = JX.$N('div', JX.$H(template));
 
       // NOTE: Operation order matters here; we can't remove anything until
       // after we draw the new rows because _draw uses the old rows to figure
       // out where to place the comment.
 
       // We use 'exact_row' to put the "undo" text directly above the affected
       // comment.
       var exact_row = true;
       var rows = this._draw(template, exact_row);
 
       this._removeUndoLink();
 
       JX.DifferentialInlineCommentEditor._undoRows = rows;
     },
 
     start : function() {
       this._registerUndoListener();
 
       var data = this._buildRequestData();
 
       var op = this.getOperation();
 
 
       if (op == 'delete') {
         this._setRowState('loading');
         var oncomplete = JX.bind(this, this._didCompleteWorkflow);
         var onclose = JX.bind(this, function() {
           this._setRowState('visible');
           this._didCancelWorkflow();
         });
 
         new JX.Workflow(this._uri, data)
           .setHandler(oncomplete)
           .setCloseHandler(onclose)
           .start();
       } else {
         var handler = JX.bind(this, this._didContinueWorkflow);
 
         if (op == 'edit') {
           this._setRowState('loading');
         }
 
         new JX.Request(this._uri, handler)
           .setData(data)
           .send();
       }
 
       return this;
     }
   },
 
   statics : {
     /**
      * Global refernece to the 'undo' rows currently rendered in the document.
      */
     _undoRows : null,
 
     /**
      * Global listener for the 'undo' click associated with the currently
      * displayed 'undo' link. When an editor is start()ed, it becomes the active
      * editor.
      */
     _activeEditor : null
   },
 
   properties : {
     operation : null,
     row : null,
+    otherRows: [],
     table : null,
     onRight : null,
     ID : null,
     lineNumber : null,
     changeset : null,
     length : null,
     isNew : null,
     text : null,
     templates : null,
     originalText : null
   }
 
 });
diff --git a/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js b/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
index e28c4ebcd..ae8bb3c04 100644
--- a/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
+++ b/webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
@@ -1,242 +1,262 @@
 /**
  * @provides javelin-behavior-differential-edit-inline-comments
  * @requires javelin-behavior
  *           javelin-stratcom
  *           javelin-dom
  *           javelin-util
  *           javelin-vector
  *           differential-inline-comment-editor
  */
 
 JX.behavior('differential-edit-inline-comments', function(config) {
 
   var selecting = false;
   var reticle = JX.$N('div', {className: 'differential-reticle'});
   JX.DOM.hide(reticle);
   document.body.appendChild(reticle);
 
   var origin = null;
   var target = null;
   var root   = null;
   var changeset = null;
 
   var editor = null;
 
   function updateReticle() {
     var top = origin;
     var bot = target;
     if (JX.$V(top).y > JX.$V(bot).y) {
       var tmp = top;
       top = bot;
       bot = tmp;
     }
     var code = target.nextSibling;
 
     var pos = JX.$V(top).add(1 + JX.Vector.getDim(target).x, 0);
     var dim = JX.Vector.getDim(code).add(-4, 0);
     if (isOnRight(target)) {
       dim.x += JX.Vector.getDim(code.nextSibling).x;
     }
     dim.y = (JX.$V(bot).y - pos.y) + JX.Vector.getDim(bot).y;
 
     pos.setPos(reticle);
     dim.setDim(reticle);
 
     JX.DOM.show(reticle);
   }
 
   function hideReticle() {
     JX.DOM.hide(reticle);
   }
 
   JX.DifferentialInlineCommentEditor.listen('done', function() {
     selecting = false;
     editor = false;
     hideReticle();
     set_link_state(false);
   });
 
   function isOnRight(node) {
     return node.parentNode.firstChild != node;
   }
 
   function isNewFile(node) {
     var data = JX.Stratcom.getData(root);
     return isOnRight(node) || (data.left != data.right);
   }
 
   function getRowNumber(th_node) {
     try {
       return parseInt(th_node.id.match(/^C\d+[ON]L(\d+)$/)[1], 10);
     } catch (x) {
       return undefined;
     }
   }
 
   var set_link_state = function(active) {
     JX.DOM.alterClass(JX.$(config.stage), 'inline-editor-active', active);
   };
 
   JX.Stratcom.listen(
     'mousedown',
     ['differential-changeset', 'tag:th'],
     function(e) {
       if (editor  ||
           selecting ||
           e.isRightButton() ||
           getRowNumber(e.getTarget()) === undefined) {
         return;
       }
 
       selecting = true;
       root = e.getNode('differential-changeset');
 
       origin = target = e.getTarget();
 
       var data = e.getNodeData('differential-changeset');
       if (isOnRight(target)) {
         changeset = data.right;
       } else {
         changeset = data.left;
       }
 
       updateReticle();
 
       e.kill();
     });
 
   JX.Stratcom.listen(
     'mouseover',
     ['differential-changeset', 'tag:th'],
     function(e) {
       if (!selecting ||
           editor ||
           (getRowNumber(e.getTarget()) === undefined) ||
           (isOnRight(e.getTarget()) != isOnRight(origin)) ||
           (e.getNode('differential-changeset') !== root)) {
         return;
       }
 
       target = e.getTarget();
 
       updateReticle();
     });
 
   JX.Stratcom.listen(
     'mouseup',
     null,
     function(e) {
       if (editor || !selecting) {
         return;
       }
 
       var o = getRowNumber(origin);
       var t = getRowNumber(target);
 
       var insert;
       var len;
       if (t < o) {
         len = (o - t);
         o = t;
         insert = origin.parentNode;
       } else {
         len = (t - o);
         insert = target.parentNode;
       }
 
       editor = new JX.DifferentialInlineCommentEditor(config.uri)
         .setTemplates(config.undo_templates)
         .setOperation('new')
         .setChangeset(changeset)
         .setLineNumber(o)
         .setLength(len)
         .setIsNew(isNewFile(target) ? 1 : 0)
         .setOnRight(isOnRight(target) ? 1 : 0)
         .setRow(insert.nextSibling)
         .setTable(insert.parentNode)
         .start();
 
       set_link_state(true);
 
       e.kill();
     });
 
   JX.Stratcom.listen(
     ['mouseover', 'mouseout'],
     'differential-inline-comment',
     function(e) {
       if (e.getType() == 'mouseout') {
         hideReticle();
       } else {
         root = e.getNode('differential-changeset');
 
         var data = e.getNodeData('differential-inline-comment');
         var change = e.getNodeData('differential-changeset');
 
         var id_part  = data.on_right ? change.right : change.left;
         var th = e.getNode('tag:td').previousSibling;
         var new_part = isNewFile(th) ? 'N' : 'O';
         var prefix = 'C' + id_part + new_part + 'L';
 
         origin = JX.$(prefix + data.number);
         target = JX.$(prefix + (parseInt(data.number, 10) +
                                 parseInt(data.length, 10)));
 
         updateReticle();
       }
     });
 
   var action_handler = function(op, e) {
     e.kill();
 
     if (editor) {
       return;
     }
 
     var node = e.getNode('differential-inline-comment');
     handle_inline_action(node, op);
   }
 
   var handle_inline_action = function(node, op) {
     var data = JX.Stratcom.getData(node);
     var row  = node.parentNode.parentNode;
+    var other_rows = [];
+    if (JX.Stratcom.hasSigil(node, 'differential-inline-comment-preview')) {
+      // The DOM structure around the comment is different if it's part of the
+      // preview, so make sure not to pass the wrong container.
+      row = node;
+      if (op === 'delete') {
+        // Furthermore, deleting a comment in the preview does not automatically
+        // delete other occurrences of the same comment, so do that manually.
+        var nodes = JX.DOM.scry(
+          document.body,
+          'div',
+          'differential-inline-comment');
+        for (var i = 0; i < nodes.length; ++i) {
+          if (JX.Stratcom.getData(nodes[i]).id === data.id) {
+            other_rows.push(nodes[i]);
+          }
+        }
+      }
+    }
 
     var original = data.original;
     if (op == 'reply') {
       // If the user hit "reply", the original text is empty (a new reply), not
       // the text of the comment they're replying to.
       original = '';
     }
 
     editor = new JX.DifferentialInlineCommentEditor(config.uri)
       .setTemplates(config.undo_templates)
       .setOperation(op)
       .setID(data.id)
       .setLineNumber(data.number)
       .setLength(data.length)
       .setOnRight(data.on_right)
       .setOriginalText(original)
       .setRow(row)
+      .setOtherRows(other_rows)
       .setTable(row.parentNode)
       .start();
 
     set_link_state(true);
   }
 
   for (var op in {'edit' : 1, 'delete' : 1, 'reply' : 1}) {
     JX.Stratcom.listen(
       'click',
       ['differential-inline-comment', 'differential-inline-' + op],
       JX.bind(null, action_handler, op));
   }
 
   JX.Stratcom.listen(
     'differential-inline-action',
     null,
     function(e) {
       var data = e.getData();
       handle_inline_action(data.node, data.op);
     });
 
 });