diff --git a/reviewboard/reviews/context.py b/reviewboard/reviews/context.py
index f982f45f05b97cd67744bd0f0dc16a8376bff70f..3e93182c04dd423f19cf9dd04bfe01daa0dfac30 100644
--- a/reviewboard/reviews/context.py
+++ b/reviewboard/reviews/context.py
@@ -1,5 +1,7 @@
 from __future__ import unicode_literals
 
+from collections import defaultdict
+
 from django.utils import six
 
 from reviewboard.attachments.forms import CommentFileForm
@@ -32,6 +34,7 @@ def comment_counts(user, all_comments, filediff, interfilediff=None):
       ==============================================================
     """
     comment_dict = {}
+    comment_replies_dict = defaultdict(list)
 
     if interfilediff:
         key = (filediff.pk, interfilediff.pk)
@@ -45,33 +48,59 @@ def comment_counts(user, all_comments, filediff, interfilediff=None):
 
         if review and (review.public or review.user == user):
             key = (comment.first_line, comment.num_lines)
+            parent_id = comment.reply_to_id
 
-            comment_dict.setdefault(key, []).append({
-                'comment_id': comment.id,
-                'text': normalize_text_for_edit(user, comment.text,
-                                                comment.rich_text),
-                'html': markdown_render_conditional(comment.text,
+            if parent_id is None:
+                comment_dict.setdefault(key, []).append({
+                    'comment_id': comment.id,
+                    'text': normalize_text_for_edit(user, comment.text,
+                                                    comment.rich_text),
+                    'html': markdown_render_conditional(comment.text,
+                                                        comment.rich_text),
+                    'rich_text': comment.rich_text,
+                    'line': comment.first_line,
+                    'num_lines': comment.num_lines,
+                    'user': {
+                        'username': review.user.username,
+                        'name': (review.user.get_full_name() or
+                                 review.user.username),
+                    },
+                    'url': comment.get_review_url(),
+                    'localdraft': (review.user == user and
+                                   not review.public),
+                    'review_id': review.id,
+                    'issue_opened': comment.issue_opened,
+                    'issue_status': BaseComment.issue_status_to_string(
+                        comment.issue_status),
+                    'replies': [],
+                })
+            else:
+                comment_replies_dict[parent_id].append({
+                    'comment_id': comment.id,
+                    'text': normalize_text_for_edit(user, comment.text,
                                                     comment.rich_text),
-                'rich_text': comment.rich_text,
-                'line': comment.first_line,
-                'num_lines': comment.num_lines,
-                'user': {
-                    'username': review.user.username,
-                    'name': (review.user.get_full_name() or
-                             review.user.username),
-                },
-                'url': comment.get_review_url(),
-                'localdraft': (review.user == user and
-                               not review.public),
-                'review_id': review.id,
-                'issue_opened': comment.issue_opened,
-                'issue_status': BaseComment.issue_status_to_string(
-                    comment.issue_status),
-            })
+                    'html': markdown_render_conditional(comment.text,
+                                                        comment.rich_text),
+                    'rich_text': comment.rich_text,
+                    'user': {
+                        'username': review.user.username,
+                        'name': (review.user.get_full_name() or
+                                 review.user.username),
+                    },
+                    'localdraft': (review.user == user and
+                                   not review.public),
+                    'review_id': review.id,
+                })
 
     comments_array = []
 
     for key, value in six.iteritems(comment_dict):
+        for comment_item in value:
+            comment_id = comment_item['comment_id']
+
+            if comment_id in comment_replies_dict:
+                comment_item['replies'] = comment_replies_dict[comment_id]
+
         comments_array.append({
             'linenum': key[0],
             'num_lines': key[1],
diff --git a/reviewboard/reviews/ui/base.py b/reviewboard/reviews/ui/base.py
index 8f7a60736cac2ed88b9c9836328b4dba4c16a74d..09df5d86f2bc398eda4c653341101aefafbe682c 100644
--- a/reviewboard/reviews/ui/base.py
+++ b/reviewboard/reviews/ui/base.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
 import json
 import logging
 import os
+from collections import defaultdict
 from uuid import uuid4
 
 import mimeparse
@@ -246,6 +247,8 @@ class ReviewUI(object):
         user = self.request.user
 
         result = []
+        comment_replies_dict = defaultdict(list)
+
         for comment in comments:
             try:
                 review = comment.get_review()
@@ -253,14 +256,26 @@ class ReviewUI(object):
                 logging.error('Missing Review for comment %r' % comment)
                 continue
 
+            parent_id = comment.reply_to_id
+
             try:
                 if review and (review.public or review.user == user):
-                    result.append(self.serialize_comment(comment))
+                    if parent_id is None:
+                        result.append(self.serialize_comment(comment))
+                    else:
+                        comment_replies_dict[parent_id].append(
+                            self.serialize_comment(comment))
             except Exception as e:
                 logging.error('Error when calling serialize_comment for '
                               'FileAttachmentReviewUI %r: %s',
                               self, e, exc_info=1)
 
+        for comment in result:
+            comment_id = comment['comment_id']
+
+            if comment_id in comment_replies_dict:
+                comment['replies'] = comment_replies_dict[comment_id]
+
         return result
 
     def serialize_comment(self, comment):
@@ -275,7 +290,7 @@ class ReviewUI(object):
         review = comment.get_review()
         user = self.request.user
 
-        return {
+        result = {
             'comment_id': comment.pk,
             'text': escape(comment.text),
             'html': markdown_render_conditional(comment.text,
@@ -293,6 +308,11 @@ class ReviewUI(object):
                 comment.issue_status),
         }
 
+        if comment.reply_to_id is None:
+            result['replies'] = []
+
+        return result
+
 
 class FileAttachmentReviewUI(ReviewUI):
     """Base class for Review UIs for file attachments.
diff --git a/reviewboard/reviews/ui/text.py b/reviewboard/reviews/ui/text.py
index 26ccea3a67de5c5bbb8e8e6e53dcbd510a124475..fb483c69ce9ebf5451edf8bb2bc65418ad9e090c 100644
--- a/reviewboard/reviews/ui/text.py
+++ b/reviewboard/reviews/ui/text.py
@@ -2,8 +2,11 @@ from __future__ import unicode_literals
 
 import logging
 
+from collections import defaultdict
+
 from django.template.context import Context
 from django.template.loader import render_to_string
+from django.utils import six
 from django.utils.safestring import mark_safe
 from djblets.cache.backend import cache_memoize
 from pygments import highlight
@@ -186,11 +189,20 @@ class TextBasedReviewUI(FileAttachmentReviewUI):
     def serialize_comments(self, comments):
         """Return a dictionary of the comments for this file attachment."""
         result = {}
+        comment_replies_dict = defaultdict(list)
 
         for comment in comments:
+            parent_id = comment.reply_to_id
+
             try:
-                key = '%s-%s' % (comment.extra_data['beginLineNum'],
-                                 comment.extra_data['endLineNum'])
+                if parent_id is None:
+                    key = '%s-%s' % (comment.extra_data['beginLineNum'],
+                                     comment.extra_data['endLineNum'])
+                    result.setdefault(key, []).append(
+                        self.serialize_comment(comment))
+                else:
+                    comment_replies_dict[parent_id].append(
+                        self.serialize_comment(comment))
             except KeyError:
                 # It's possible this comment was made before the review UI
                 # was provided, meaning it has no data. If this is the case,
@@ -198,7 +210,12 @@ class TextBasedReviewUI(FileAttachmentReviewUI):
                 # line region.
                 continue
 
-            result.setdefault(key, []).append(self.serialize_comment(comment))
+        for key, value in six.iteritems(result):
+            for comment_item in value:
+                comment_id = comment_item['comment_id']
+
+                if comment_id in comment_replies_dict:
+                    comment_item['replies'] = comment_replies_dict[comment_id]
 
         return result
 
diff --git a/reviewboard/static/rb/css/pages/reviews.less b/reviewboard/static/rb/css/pages/reviews.less
index 52e017bf6e731f915088affcac7649e1b7ef35e3..32616f71567607cd8272fbb673c01f9f2d271a66 100644
--- a/reviewboard/static/rb/css/pages/reviews.less
+++ b/reviewboard/static/rb/css/pages/reviews.less
@@ -1668,7 +1668,7 @@
     border-right: 1px black solid;
     padding: 6px;
 
-    ul {
+    > ul {
       background-color: white;
       border: 1px #333333 solid;
       list-style: none;
@@ -1677,13 +1677,9 @@
       padding: 0;
       position: relative;
 
-      li {
+      > li {
         padding: 6px;
 
-        &.even {
-          background-color: #F3F3F3;
-        }
-
         h2 {
           font-size: 90%;
           margin: 0 0 6px 0;
@@ -1714,6 +1710,69 @@
           .pre-wrap;
           font-size: 90%;
         }
+
+        .reply-comments {
+          list-style: none;
+          padding-left: 5px;
+          overflow: auto;
+          margin-top: 10px;
+          border-left: 2px #D0D0D0 solid;
+
+          /*
+           * Set a border-bottom to prevent a bug in IE where, without a
+           * border-bottom, the content of the dl will shift left by the
+           * border-left size for every following item.
+           */
+          border-bottom: 1px #FAFAFA solid;
+
+          li {
+            .reviewer {
+              color: #AB5603;
+            }
+
+            &.draft {
+              .reviewer {
+                color: #4E9A06;
+
+                label {
+                .inline-editor-label();
+                }
+              }
+
+              .inline-editor-form {
+                padding: 0;
+
+                .buttons {
+                  position: relative;
+                  bottom: 0;
+                  left: 0;
+
+                  > input {
+                    margin-top: 2em;
+                  }
+
+                  .enable-markdown {
+                    position: absolute;
+                    margin: 0;
+                    top: 0;
+                    left: 0;
+                  }
+
+                  .markdown-info {
+                    margin: 0;
+                    width: 40%;
+                    text-align: right;
+                  }
+                }
+
+              }
+            }
+          }
+        }
+      }
+
+      > li:nth-child(even) {
+        background-color: #F3F3F3;
       }
     }
   }
@@ -1869,12 +1928,27 @@
         border-bottom: 1px #5170b3 solid;
         margin: 0;
         padding: 5px;
+        position: relative;
 
         &.draft {
           background-color: #aceb6f;
           border-bottom: 1px black solid;
         }
 
+        &.reply:before {
+          content: "";
+          position: absolute;
+          top: 5px;
+          bottom: 5px;
+          left: 5px;
+          width: 2px;
+          background-color: #5170b3;
+        }
+
+        &.reply {
+          padding-left: 15px;
+        }
+
         div {
           &.reviewer {
             font-weight: bold;
diff --git a/reviewboard/static/rb/js/views/abstractCommentBlockView.js b/reviewboard/static/rb/js/views/abstractCommentBlockView.js
index 95d79a3e8af44ef6a6a18e1ec0936e92f3505f93..79f8df47d99b0d37c0ef8ad840da4a73c51fabe5 100644
--- a/reviewboard/static/rb/js/views/abstractCommentBlockView.js
+++ b/reviewboard/static/rb/js/views/abstractCommentBlockView.js
@@ -32,6 +32,7 @@ RB.AbstractCommentBlockView = Backbone.View.extend({
         this.model.on('change:draftComment', this._onDraftCommentChanged, this);
         this._onDraftCommentChanged();
 
+        this.model.on('change:serializedComments', this._updateTooltip, this);
         this._updateTooltip();
 
         return this;
@@ -104,7 +105,11 @@ RB.AbstractCommentBlockView = Backbone.View.extend({
             draftComment = this.model.get('draftComment'),
             userSession = RB.UserSession.instance,
             tooltipTemplate = _.template([
-                '<li>',
+                '<% if (isDraft) { %>',
+                ' <li class="draft">',
+                '<% } else { %>',
+                ' <li>',
+                '<% } %>',
                 ' <div class="reviewer">',
                 '  <%- user %>:',
                 ' </div>',
@@ -115,18 +120,28 @@ RB.AbstractCommentBlockView = Backbone.View.extend({
         if (draftComment) {
             $(tooltipTemplate({
                 user: userSession.get('fullName'),
-                html: draftComment.get('html')
+                html: draftComment.get('html'),
+                isDraft: true
             }))
-            .addClass('draft')
             .appendTo($list);
         }
 
         _.each(this.model.get('serializedComments'), function(comment) {
             $(tooltipTemplate({
                 user: comment.user.name,
-                html: comment.html
+                html: comment.html,
+                isDraft: false
             }))
             .appendTo($list);
+            _.each(comment.replies, function(reply) {
+                $(tooltipTemplate({
+                    user: reply.user.name,
+                    html: reply.html,
+                    isDraft: reply.localdraft
+                }))
+                .addClass('reply')
+                .appendTo($list);
+            })
         });
 
         this._$tooltip
diff --git a/reviewboard/static/rb/js/views/abstractReviewableView.js b/reviewboard/static/rb/js/views/abstractReviewableView.js
index 9b9fbc01f69bf13dba8c107d7ec4ccf2bda21b10..abbc5bc73f1714ceb92d379d27acd8fe0385c91c 100644
--- a/reviewboard/static/rb/js/views/abstractReviewableView.js
+++ b/reviewboard/static/rb/js/views/abstractReviewableView.js
@@ -94,6 +94,7 @@ RB.AbstractReviewableView = Backbone.View.extend({
         this.commentDlg = RB.CommentDialogView.create({
             comment: commentBlock.get('draftComment'),
             publishedComments: commentBlock.get('serializedComments'),
+            commentBlock: commentBlock,
             publishedCommentsType: this.commentsListName,
             position: function(dlg) {
                 commentBlockView.positionCommentDlg(dlg);
diff --git a/reviewboard/static/rb/js/views/commentDialogReviewView.js b/reviewboard/static/rb/js/views/commentDialogReviewView.js
new file mode 100644
index 0000000000000000000000000000000000000000..83fc6d4618a7620c5b22055777cad6eb4ef83542
--- /dev/null
+++ b/reviewboard/static/rb/js/views/commentDialogReviewView.js
@@ -0,0 +1,181 @@
+/*
+ *
+ */
+RB.CommentDialogReviewView = Backbone.View.extend({
+    itemTemplate: _.template([
+        '<li>',
+         '<h2>',
+          '<span class="user"><%- comment.user.name %></span>',
+          '<span class="actions">',
+           '<a href="<%= comment.url %>"><%- viewText %></a>',
+           '<a class="add_comment_link">Reply</a>',
+          '</span>',
+         '</h2>',
+         '<pre class="rich-text"><%= comment.html %></pre>'
+    ].join('')),
+
+    replyTemplate: _.template([
+        '<li <% if (isDraft) { %> class="draft" <% } %>>',
+         '<h2 class="reviewer">',
+          '<label for="<%= id %>">',
+           '<span class="user"><%- fullName %></span>',
+          '</label>',
+         '</h2>',
+         '<pre id="<%= id %>" class="rich-text reviewtext"><%= text %></pre>'
+    ].join('')),
+
+    initialize: function() {
+        this._reviewReply = null;
+
+        this._setupNewReply(this.options.reviewReply);
+    },
+
+    render: function() {
+        var serializedComment = this.options.serializedComment,
+            replyType = this.options.replyType,
+            reviewRequestURL = this.options.reviewRequestURL,
+            commentIssueManager = this.options.commentIssueManager,
+            interactive = this.options.interactive,
+            $replies = $('<ul/>').addClass('reply-comments'),
+            $replyItem,
+            commentIssueBar;
+
+        this.$el = $(this.itemTemplate({
+            comment: serializedComment,
+            reviewRequestURL: reviewRequestURL,
+            replyType: replyType,
+            viewText: gettext('View')
+        }));
+
+        if (serializedComment.issue_opened) {
+            commentIssueBar = new RB.CommentIssueBarView({
+                reviewID: serializedComment.review_id,
+                commentID: serializedComment.comment_id,
+                commentType: replyType,
+                issueStatus: serializedComment.issue_status,
+                interactive: interactive,
+                commentIssueManager: commentIssueManager
+            });
+            commentIssueBar.render().$el.appendTo(this.$el);
+
+            /*
+             * Update the serialized comment's issue status whenever
+             * the real comment's status is changed so we will
+             * display it correctly the next time we render it.
+             */
+            commentIssueManager.on('issueStatusUpdated',
+                                   function (comment) {
+                if (comment.id === serializedComment.commentID) {
+                    serializedComment.issue_status =
+                        comment.get('issueStatus');
+                }
+            });
+        }
+
+        _.each(serializedComment.replies, function (serializedReply) {
+            $replyItem = $(this.replyTemplate({
+                id: serializedReply.comment_id,
+                fullName: serializedReply.user.name,
+                text: serializedReply.html,
+                isDraft: serializedReply.localdraft
+            }))
+            .attr('data-comment-id', serializedReply.comment_id);
+
+            $replyItem.appendTo($replies)
+                .find('pre.reviewtext')
+                .data('raw-value', serializedReply.text);
+        }, this);
+
+        $replies.appendTo(this.$el);
+    },
+
+    /*
+     * Sets up a new ReviewReply for the editors.
+     *
+     * The new ReviewReply will be used for any new comments made on this
+     * review.
+     *
+     * A ReviewReply is set until it's either destroyed or published, at
+     * which point a new one is set.
+     *
+     * A ReviewReply can be provided to this function, and if not supplied,
+     * a new one will be created.
+     */
+    _setupNewReply: function(reviewReply) {
+        var hadReviewReply = (this._reviewReply !== null);
+
+        if (!reviewReply) {
+            reviewReply = this.model.createReply();
+        }
+
+        if (hadReviewReply) {
+            this.stopListening(this._reviewReply);
+        }
+
+        this.listenTo(reviewReply, 'destroyed published', function() {
+            this._setupNewReply();
+        });
+
+        this._reviewReply = reviewReply;
+    },
+
+    /*
+     * Renders the review reply editor.
+     *
+     * This is done separately from the view rendering as the inline editor
+     * needs to be created when the review is visible, so the rendering of
+     * the editor must be done when the comment dialog is opened.
+     */
+    renderReviewEditor: function() {
+        var serializedComment = this.options.serializedComment,
+            replyType = this.options.replyType,
+            editor,
+            editorView;
+
+        editor = new RB.ReviewReplyEditor({
+            contextID: serializedComment.comment_id,
+            contextType: replyType,
+            review: this.model,
+            reviewReply: this._reviewReply
+        });
+        editorView = new RB.ReviewReplyEditorView({
+            el: this.$el,
+            model: editor,
+            commentTemplate: this.replyTemplate
+        });
+        editorView.render();
+
+        editor.on('saved', function() {
+            var replyObj = editor.get('replyObject'),
+                serializedReplies = serializedComment.replies,
+                reply,
+                userSession;
+
+            if (serializedReplies.length > 0) {
+                 reply = serializedReplies[serializedReplies.length - 1];
+            }
+
+            if (reply && reply.localdraft) {
+                reply.html = replyObj.get('text');
+                reply.text = replyObj.get('rawTextFields').text;
+                reply.richText = replyObj.get('richText');
+            } else {
+                userSession = RB.UserSession.instance;
+                serializedReplies.push({
+                    comment_id: replyObj.id,
+                    html: replyObj.get('text'),
+                    localdraft: true,
+                    review_id: replyObj.get('replyToID'),
+                    rich_text: replyObj.get('richText'),
+                    text: replyObj.get('rawTextFields').text,
+                    user: {
+                        name: userSession.get('fullName'),
+                        username: userSession.get('username')
+                    }
+                })
+            }
+
+            this.options.event.trigger('draftReplyChanged');
+        }, this);
+    }
+});
\ No newline at end of file
diff --git a/reviewboard/static/rb/js/views/commentDialogView.js b/reviewboard/static/rb/js/views/commentDialogView.js
index c444dc216dce7597596db408d33ce91d4bb47da6..2ef3526a037a6a7f3bb7cb12dbf3320a575685e5 100644
--- a/reviewboard/static/rb/js/views/commentDialogView.js
+++ b/reviewboard/static/rb/js/views/commentDialogView.js
@@ -9,78 +9,60 @@
  * This is used internally in CommentDialogView.
  */
 var CommentsListView = Backbone.View.extend({
-    itemTemplate: _.template([
-        '<li class="<%= itemClass %>">',
-         '<h2>',
-          '<%- comment.user.name %>',
-          '<span class="actions">',
-           '<a href="<%= comment.url %>"><%- viewText %></a>',
-           '<a href="<%= reviewRequestURL %>',
-                    '?reply_id=<%= comment.comment_id %>',
-                    '&reply_type=<%= replyType %>">Reply</a>',
-          '</span>',
-         '</h2>',
-         '<pre><%- comment.text %></pre>'
-    ].join('')),
+    event: $.extend({}, Backbone.Events),
 
     /*
      * Set the list of displayed comments.
      */
     setComments: function(comments, replyType) {
-        var reviewRequestURL = this.options.reviewRequestURL,
+        var reviewRequest = this.options.reviewRequest,
+            reviewRequestURL = this.options.reviewRequestURL,
             commentIssueManager = this.options.commentIssueManager,
             interactive = this.options.issuesInteractive,
-            odd = true,
             $items = $();
 
         if (comments.length === 0) {
             return;
         }
 
+        this.reviewViews = [];
+
         _.each(comments, function(serializedComment) {
-            var commentID = serializedComment.comment_id,
-                $item = $(this.itemTemplate({
-                    comment: serializedComment,
-                    itemClass: odd ? 'odd' : 'even',
-                    reviewRequestURL: reviewRequestURL,
-                    replyType: replyType,
-                    viewText: gettext('View')
-                })),
-                commentIssueBar;
-
-            if (serializedComment.issue_opened) {
-                commentIssueBar = new RB.CommentIssueBarView({
-                    reviewID: serializedComment.review_id,
-                    commentID: commentID,
-                    commentType: replyType,
-                    issueStatus: serializedComment.issue_status,
-                    interactive: interactive,
-                    commentIssueManager: commentIssueManager
-                });
-                commentIssueBar.render().$el.appendTo($item);
+            var reviewID = serializedComment.review_id,
+                review,
+                reviewComments;
+
+            review = reviewRequest.createReview(reviewID);
+            reviewComments = new RB.CommentDialogReviewView({
+                model: review,
+                serializedComment: serializedComment,
+                replyType: replyType,
+                reviewRequest: reviewRequest,
+                reviewRequestURL: reviewRequestURL,
+                commentIssueManager: commentIssueManager,
+                interactive: interactive,
+                event: this.event
+            });
 
-                /*
-                 * Update the serialized comment's issue status whenever
-                 * the real comment's status is changed so we will
-                 * display it correctly the next time we render it.
-                 */
-                commentIssueManager.on('issueStatusUpdated',
-                                       function(comment) {
-                    if (comment.id === commentID) {
-                        serializedComment.issue_status =
-                            comment.get('issueStatus');
-                    }
-                });
-            }
+            reviewComments.render();
 
-            $items = $items.add($item);
+            this.reviewViews.push(reviewComments);
 
-            odd = !odd;
+            $items = $items.add(reviewComments.$el);
         }, this);
 
         this.$el
             .empty()
             .append($items);
+    },
+
+    /*
+     * Renders the review reply editors for each review.
+     */
+    renderReviewEditors: function() {
+        _.each(this.reviewViews, function(reviewView) {
+            reviewView.renderReviewEditor();
+        })
     }
 });
 
@@ -106,7 +88,7 @@ RB.CommentDialogView = Backbone.View.extend({
         ' <h1 class="title"><%- otherReviewsText %></h1>',
         ' <ul></ul>',
         '</div>',
-        '<form method="post">',
+        '<form class="new-diff-comment" method="post">',
         ' <h1 class="title">',
         '  <%- yourCommentText %>',
         '<% if (authenticated && !hasDraft) { %>',
@@ -143,10 +125,10 @@ RB.CommentDialogView = Backbone.View.extend({
     ].join('')),
 
     events: {
-        'click .buttons .cancel': '_onCancelClicked',
-        'click .buttons .close': '_onCancelClicked',
-        'click .buttons .delete': '_onDeleteClicked',
-        'click .buttons .save': 'save',
+        'click .new-diff-comment .buttons .cancel': '_onCancelClicked',
+        'click .new-diff-comment .buttons .close': '_onCancelClicked',
+        'click .new-diff-comment .buttons .delete': '_onDeleteClicked',
+        'click .new-diff-comment .buttons .save': 'save',
         'keydown .comment-text-field': '_onTextKeyDown'
     },
 
@@ -233,11 +215,16 @@ RB.CommentDialogView = Backbone.View.extend({
 
         this.commentsList = new CommentsListView({
             el: this._$commentsPane.find('ul'),
+            reviewRequest: reviewRequest,
             reviewRequestURL: reviewRequest.get('reviewURL'),
             commentIssueManager: this.options.commentIssueManager,
             issuesInteractive: reviewRequestEditor.get('editable')
         });
 
+        this.listenTo(this.commentsList.event, 'draftReplyChanged', function() {
+            this.options.commentBlock.trigger('change:serializedComments');
+        });
+
         /*
          * We need to handle keypress here, rather than in events above,
          * because jQuery will actually handle it. Backbone fails to.
@@ -349,6 +336,7 @@ RB.CommentDialogView = Backbone.View.extend({
         function openDialog() {
             this.$el.scrollIntoView();
             this._textEditor.focus();
+            this.commentsList.renderReviewEditors();
         }
 
         this.$el
@@ -621,6 +609,7 @@ RB.CommentDialogView = Backbone.View.extend({
         dlg = new RB.CommentDialogView({
             animate: options.animate,
             commentIssueManager: commentIssueManager,
+            commentBlock: options.commentBlock,
             model: new RB.CommentEditor({
                 comment: options.comment,
                 reviewRequest: reviewRequestEditor.get('reviewRequest'),
diff --git a/reviewboard/static/rb/js/views/reviewReplyEditorView.js b/reviewboard/static/rb/js/views/reviewReplyEditorView.js
index 3f0b23c575e7ffc6096224bf092d65476170da26..e55f31f5ec793df3c5d75b8ce2788c152875c04e 100644
--- a/reviewboard/static/rb/js/views/reviewReplyEditorView.js
+++ b/reviewboard/static/rb/js/views/reviewReplyEditorView.js
@@ -35,6 +35,10 @@ RB.ReviewReplyEditorView = Backbone.View.extend({
         this._$draftComment = null;
         this._$editor = null;
         this._$commentsList = null;
+
+        if (this.options.commentTemplate) {
+            this.commentTemplate = this.options.commentTemplate;
+        }
     },
 
     /*
diff --git a/reviewboard/staticbundles.py b/reviewboard/staticbundles.py
index 2b9b32058a70fba8233834a7ff4e89bda6205034..e98b1f24a942df48055ef796d20429d08916e2fb 100644
--- a/reviewboard/staticbundles.py
+++ b/reviewboard/staticbundles.py
@@ -201,6 +201,7 @@ PIPELINE_JS = dict({
             'rb/js/views/abstractCommentBlockView.js',
             'rb/js/views/abstractReviewableView.js',
             'rb/js/views/collapsableBoxView.js',
+            'rb/js/views/commentDialogReviewView.js',
             'rb/js/views/commentDialogView.js',
             'rb/js/views/commentIssueBarView.js',
             'rb/js/views/diffFragmentQueueView.js',
