diff --git a/trac/htdocs/css/ticket.css b/trac/htdocs/css/ticket.css
--- a/trac/htdocs/css/ticket.css
+++ b/trac/htdocs/css/ticket.css
@@ -110,6 +110,7 @@ div.comment ol { list-style: decimal }
 }
 #trac-comment-editor .wikitoolbar { margin-left: -1px }
 #trac-add-comment :link, #trac-add-comment :visited { color: #b00 }
+#changelog .trac-new { border-left: 0.3em solid #c0f0c0; padding-left: 0.3em; }
 #changelog h3, #ticketchange h3 {
  border-bottom: 1px solid #d7d7d7;
  color: #999;
@@ -137,6 +138,7 @@ form .field .wikitoolbar { margin-left: 
 form .field div.trac-resizable { width: 100% }
 
 #propertyform { margin-bottom: 2em; }
+#trac-edit-warning .trac-new { background-color: #c0f0c0; }
 #properties { white-space: nowrap; line-height: 160%; padding: .5em }
 #properties table { border-spacing: 0; width: 100%; padding: 0 .5em }
 #properties table th {
diff --git a/trac/htdocs/js/threaded_comments.js b/trac/htdocs/js/threaded_comments.js
--- a/trac/htdocs/js/threaded_comments.js
+++ b/trac/htdocs/js/threaded_comments.js
@@ -3,8 +3,7 @@ jQuery(document).ready(function($){
   var toggle = $('#trac-threaded-toggle');
   toggle.click(function() {
     if ($(this).checked()) {
-      if (!comments)
-        comments = $("div.change");
+      comments = $("div.change");
       comments.each(function() {
         var children = $("a.follow-up", this).map(function() {
           var cnum = $(this).attr("href").replace('#comment:', '');
diff --git a/trac/ticket/model.py b/trac/ticket/model.py
--- a/trac/ticket/model.py
+++ b/trac/ticket/model.py
@@ -255,7 +255,7 @@ class Ticket(object):
         return self.id
 
     def save_changes(self, author=None, comment=None, when=None, db=None,
-                     cnum=''):
+                     cnum='', replyto=None):
         """
         Store ticket changes in the database. The ticket must already exist in
         the database.  Returns False if there were no changes to save, True
@@ -263,6 +263,8 @@ class Ticket(object):
 
         :since 0.13: the `db` parameter is no longer needed and will be removed
         in version 0.14
+        :since 0.13: the `cnum` parameter is deprecated, and threading should
+        be controled with the new `replyto` argument
         """
         assert self.exists, "Cannot update a new ticket"
 
@@ -296,9 +298,11 @@ class Ticket(object):
                     pass
 
         with self.env.db_transaction as db:
+            db("UPDATE ticket SET changetime=%s WHERE id=%s",
+               (when_ts, self.id))
+            
             # find cnum if it isn't provided
-            comment_num = cnum
-            if not comment_num:
+            if not cnum:
                 num = 0
                 for ts, old in db("""
                         SELECT DISTINCT tc1.time, COALESCE(tc2.oldvalue,'')
@@ -314,7 +318,9 @@ class Ticket(object):
                         break
                     except ValueError:
                         num += 1
-                comment_num = str(num + 1)
+                cnum = str(num + 1)
+                if replyto:
+                    cnum = '%s.%s' % (replyto, cnum)
 
             # store fields
             for name in self._old.keys():
@@ -344,10 +350,7 @@ class Ticket(object):
             db("""INSERT INTO ticket_change
                     (ticket,time,author,field,oldvalue,newvalue)
                   VALUES (%s,%s,%s,'comment',%s,%s)
-                  """, (self.id, when_ts, author, comment_num, comment))
-
-            db("UPDATE ticket SET changetime=%s WHERE id=%s",
-               (when_ts, self.id))
+                  """, (self.id, when_ts, author, cnum, comment))
 
         old_values = self._old
         self._old = {}
@@ -355,7 +358,7 @@ class Ticket(object):
 
         for listener in TicketSystem(self.env).change_listeners:
             listener.ticket_changed(self, comment, author, old_values)
-        return True
+        return int(cnum.rsplit('.', 1)[-1])
 
     def get_changelog(self, when=None, db=None):
         """Return the changelog as a list of tuples of the form
diff --git a/trac/ticket/templates/ticket.html b/trac/ticket/templates/ticket.html
--- a/trac/ticket/templates/ticket.html
+++ b/trac/ticket/templates/ticket.html
@@ -39,10 +39,24 @@
                      .blur(function() { comment_focused = false; });
         $("#propertyform").autoSubmit({preview: '1'}, function(data, reply) {
           var items = $(reply);
+          // Update ticket box
           $("#ticket").replaceWith(items.filter('#ticket'));
-          var changes = $("#ticketchange-content").html(items.filter('ul.changes, div.comment'));
-          var show_preview = changes.children().length != 0;
+          // Unthread and update changelog
+          var threaded_toggle = $('#trac-threaded-toggle');
+          if (threaded_toggle.checked())
+            threaded_toggle.attr('checked', false).click().attr('checked', false);
+          $("#changelog .trac-new").remove();
+          var new_changes = items.filter("#new-changes").children();
+          $("#changelog").append(new_changes);
+          // Show warning
+          $("#trac-edit-warning").toggle(new_changes.length != 0);
+          // Update view time
+          $("#propertyform input[name='view_time']").replaceWith(items.filter("input[name='view_time']"));
+          // Update preview
+          var preview = $("#ticketchange-content").html(items.filter('#preview').children());
+          var show_preview = preview.children().length != 0;
           $("#ticketchange").toggle(show_preview);
+          // Collapse property form if comment editor has focus
           if (show_preview && comment_focused)
             $("#modify").parent().addClass("collapsed");
         });
@@ -52,7 +66,6 @@
         });
         /*]]>*/
         <py:if test="preview_mode">
-        $("#changelog").parent().toggleClass("collapsed");
         $("#attachments").toggleClass("collapsed");
         $("#trac-add-comment").scrollToTop();
         </py:if>
@@ -128,78 +141,7 @@
           <h2 class="foldable">Change History</h2>
 
           <div id="changelog">
-            <py:for each="change in changes"
-                    py:with="can_edit_comment = has_edit_comment or (authname and authname != 'anonymous'
-                                                                     and authname == change.author);
-                             show_editor = can_edit_comment and str(change.cnum) == cnum_edit;
-                             show_history = str(change.cnum) == cnum_hist;
-                             max_version = max(change.comment_history);
-                             comment_version = int(cversion or 0) if show_history else max_version;
-                             show_buttons = not show_editor and comment_version == max_version">
-              <div class="change${not show_buttons and ' trac-nobuttons' or ''}"
-                   id="${'trac-change-%d' % change.cnum if 'cnum' in change else None}">
-                <h3 class="change">
-                  <span class="threading" py:if="'cnum' in change"
-                        py:with="change_replies = replies.get(str(change.cnum), [])">
-                    <span id="comment:$change.cnum" class="cnum">${commentref('comment:', change.cnum)}</span>
-                    <py:if test="change_replies or 'replyto' in change">
-                      <py:if test="'replyto' in change">
-                        in reply to: ${commentref('&uarr;&nbsp;', change.replyto)}
-                        <py:if test="change_replies">; </py:if>
-                      </py:if>
-                      <py:if test="change_replies">
-                        <i18n:choose numeral="len(change_replies)">
-                          <span i18n:singular="">follow-up:</span>
-                          <span i18n:plural="">follow-ups:</span>
-                        </i18n:choose>
-                        <py:for each="reply in change_replies">
-                          ${commentref('&darr;&nbsp;', reply, 'follow-up')}
-                        </py:for></py:if>
-                    </py:if>
-                  </span>
-                  <i18n:msg params="date, author">Changed ${pretty_dateinfo(change.date)} by ${authorinfo(change.author)}</i18n:msg>
-                </h3>
-                <py:if test="show_buttons">
-                  <form py:if="'cnum' in change and can_edit_comment" method="get" action="#comment:${change.cnum}">
-                    <div class="inlinebuttons">
-                      <input type="hidden" name="cnum_edit" value="${change.cnum}"/>
-                      <input type="submit" value="${_('Edit')}" title="${_('Edit comment %(cnum)s', cnum=change.cnum)}"/>
-                    </div>
-                  </form>
-                  <form py:if="'cnum' in change and can_append" id="reply-to-comment-${change.cnum}"
-                        method="get" action="#comment">
-                    <div class="inlinebuttons">
-                      <input type="hidden" name="replyto" value="${change.cnum}"/>
-                      <input type="submit" value="${_('Reply')}" title="${_('Reply to comment %(cnum)s', cnum=change.cnum)}"/>
-                    </div>
-                  </form>
-                </py:if>
-                <xi:include href="ticket_change.html"/>
-                <div py:if="not show_editor and len(change.comment_history) > 1" py:choose=""
-                     class="trac-lastedit ${'trac-shade' if comment_version != max_version else None}">
-                  <i18n:msg params="version, date, author" py:when="comment_version != max_version">
-                      Version ${comment_version}, edited ${pretty_dateinfo(change.comment_history[comment_version].date)}
-                      by ${authorinfo(change.comment_history[comment_version].author)}
-                  </i18n:msg>
-                  <i18n:msg params="date, author" py:otherwise="">
-                      Last edited ${pretty_dateinfo(change.comment_history[comment_version].date)}
-                      by ${authorinfo(change.comment_history[comment_version].author)}
-                  </i18n:msg>
-                  <py:if test="comment_version > 0">
-                    (<a href="${href.ticket(ticket.id, cnum_hist=change.cnum, cversion=comment_version - 1)
-                               }#comment:${change.cnum}">previous</a>)
-                  </py:if>
-                  <py:if test="comment_version &lt; max_version">
-                    (<a href="${href.ticket(ticket.id, cnum_hist=change.cnum, cversion=comment_version + 1)
-                               }#comment:${change.cnum}">next</a>)
-                  </py:if>
-                  <py:if test="comment_version > 0">
-                    (<a href="${href.ticket(ticket.id, action='comment-diff', cnum=change.cnum,
-                                            version=comment_version)}">diff</a>)
-                  </py:if>
-                </div>
-              </div>
-            </py:for>
+            <xi:include href="ticket_changes.html"/>
           </div>
         </div>
       </py:if>
@@ -216,6 +158,11 @@
           <h2 id="trac-add-comment">
             <a id="edit" onfocus="$('#comment').get(0).focus()">Add a comment</a>
           </h2>
+          <div id="trac-edit-warning" class="warning system-message"
+               style="${'display: none' if start_time == ticket['changetime'] else None}">
+            This ticket has been modified since you started editing. The
+            <span class="trac-new">other modifications</span> are highlighted.
+          </div>
           <!--! Comment field -->
           <fieldset class="iefix">
             <label for="comment" i18n:msg="">You may use
@@ -225,10 +172,6 @@
             <textarea id="comment" name="comment" class="wikitext trac-resizable" rows="10" cols="78">
 ${comment}</textarea>
           </fieldset>
-          <div py:if="preview_mode and chrome.warnings" i18n:msg="" class="warning system-message">
-            Warnings are shown at the <a href="#warning">top of the page</a>. The ticket validation
-            may have failed.
-          </div>
         </div>
 
         <div>
@@ -411,9 +354,9 @@
         </div>
         <div class="buttons">
           <py:if test="ticket.exists">
-            <input type="hidden" name="ts" value="${timestamp}" />
+            <input type="hidden" name="start_time" value="${to_utimestamp(start_time)}" />
+            <input type="hidden" name="view_time" value="${to_utimestamp(ticket['changetime'])}" />
             <input type="hidden" name="replyto" value="${replyto}" />
-            <input type="hidden" name="cnum" value="${cnum}" />
           </py:if>
           <input type="submit" name="preview" value="${_('Preview')}" accesskey="r" />&nbsp;
           <input type="submit" name="submit" value="${_('Submit changes') if ticket.exists else _('Create ticket')}" />
diff --git a/trac/ticket/templates/ticket_change.html b/trac/ticket/templates/ticket_change.html
--- a/trac/ticket/templates/ticket_change.html
+++ b/trac/ticket/templates/ticket_change.html
@@ -4,7 +4,7 @@ Render a ticket comment.
 Arguments:
  - change: the change data
  - show_editor=False: if True, show a comment editor
- - edited_comment: the current value of the comment edito
+ - edited_comment: the current value of the comment editor
  - cnum_edit: the comment number being edited
 -->
 <html xmlns="http://www.w3.org/1999/xhtml"
diff --git a/trac/ticket/templates/ticket_changes.html b/trac/ticket/templates/ticket_changes.html
new file mode 100644
--- /dev/null
+++ b/trac/ticket/templates/ticket_changes.html
@@ -0,0 +1,92 @@
+<!--!
+Render a list of ticket changes.
+
+Arguments:
+ - changes: the list of changes
+ - edited_comment: the current value of the comment edito
+ - cnum_edit: the comment number being edited
+ - can_append: True if the user is allowed to append to tickets
+ - has_edit_comment: True if the user is allowed to edit comments
+-->
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:i18n="http://genshi.edgewall.org/i18n"
+      py:strip="">
+  <py:def function="commentref(prefix, cnum, cls=None)">
+    <a href="#comment:$cnum" class="$cls">$prefix$cnum</a>
+  </py:def>
+  <py:for each="change in changes"
+          py:with="can_edit_comment = has_edit_comment or (authname and authname != 'anonymous'
+                                                           and authname == change.author);
+                   show_editor = can_edit_comment and str(change.cnum) == cnum_edit;
+                   show_history = str(change.cnum) == cnum_hist;
+                   max_version = max(change.comment_history);
+                   comment_version = int(cversion or 0) if show_history else max_version;
+                   show_buttons = not show_editor and comment_version == max_version">
+    <div class="change${' trac-nobuttons' if not show_buttons else None}${
+                        ' trac-new' if change.date > start_time else None}"
+         id="${'trac-change-%d' % change.cnum if 'cnum' in change else None}">
+      <h3 class="change">
+        <span class="threading" py:if="'cnum' in change"
+              py:with="change_replies = replies.get(str(change.cnum), [])">
+          <span id="comment:$change.cnum" class="cnum">${commentref('comment:', change.cnum)}</span>
+          <py:if test="change_replies or 'replyto' in change">
+            <py:if test="'replyto' in change">
+              in reply to: ${commentref('&uarr;&nbsp;', change.replyto)}
+              <py:if test="change_replies">; </py:if>
+            </py:if>
+            <py:if test="change_replies">
+              <i18n:choose numeral="len(change_replies)">
+                <span i18n:singular="">follow-up:</span>
+                <span i18n:plural="">follow-ups:</span>
+              </i18n:choose>
+              <py:for each="reply in change_replies">
+                ${commentref('&darr;&nbsp;', reply, 'follow-up')}
+              </py:for></py:if>
+          </py:if>
+        </span>
+        <i18n:msg params="date, author">Changed ${pretty_dateinfo(change.date)} by ${authorinfo(change.author)}</i18n:msg>
+      </h3>
+      <py:if test="show_buttons">
+        <form py:if="'cnum' in change and can_edit_comment" method="get" action="#comment:${change.cnum}">
+          <div class="inlinebuttons">
+            <input type="hidden" name="cnum_edit" value="${change.cnum}"/>
+            <input type="submit" value="${_('Edit')}" title="${_('Edit comment %(cnum)s', cnum=change.cnum)}"/>
+          </div>
+        </form>
+        <form py:if="'cnum' in change and can_append" id="reply-to-comment-${change.cnum}"
+              method="get" action="#comment">
+          <div class="inlinebuttons">
+            <input type="hidden" name="replyto" value="${change.cnum}"/>
+            <input type="submit" value="${_('Reply')}" title="${_('Reply to comment %(cnum)s', cnum=change.cnum)}"/>
+          </div>
+        </form>
+      </py:if>
+      <xi:include href="ticket_change.html"/>
+      <div py:if="not show_editor and len(change.comment_history) > 1" py:choose=""
+           class="trac-lastedit ${'trac-shade' if comment_version != max_version else None}">
+        <i18n:msg params="version, date, author" py:when="comment_version != max_version">
+            Version ${comment_version}, edited ${pretty_dateinfo(change.comment_history[comment_version].date)}
+            by ${authorinfo(change.comment_history[comment_version].author)}
+        </i18n:msg>
+        <i18n:msg params="date, author" py:otherwise="">
+            Last edited ${pretty_dateinfo(change.comment_history[comment_version].date)}
+            by ${authorinfo(change.comment_history[comment_version].author)}
+        </i18n:msg>
+        <py:if test="comment_version > 0">
+          (<a href="${href.ticket(ticket.id, cnum_hist=change.cnum, cversion=comment_version - 1)
+                     }#comment:${change.cnum}">previous</a>)
+        </py:if>
+        <py:if test="comment_version &lt; max_version">
+          (<a href="${href.ticket(ticket.id, cnum_hist=change.cnum, cversion=comment_version + 1)
+                     }#comment:${change.cnum}">next</a>)
+        </py:if>
+        <py:if test="comment_version > 0">
+          (<a href="${href.ticket(ticket.id, action='comment-diff', cnum=change.cnum,
+                                  version=comment_version)}">diff</a>)
+        </py:if>
+      </div>
+    </div>
+  </py:for>
+</html>
diff --git a/trac/ticket/templates/ticket_preview.html b/trac/ticket/templates/ticket_preview.html
--- a/trac/ticket/templates/ticket_preview.html
+++ b/trac/ticket/templates/ticket_preview.html
@@ -6,7 +6,15 @@ to a ticket change auto-preview.
       xmlns:py="http://genshi.edgewall.org/"
       xmlns:xi="http://www.w3.org/2001/XInclude"
       xmlns:i18n="http://genshi.edgewall.org/i18n"
+      py:with="can_append = 'TICKET_APPEND' in perm(ticket.resource);
+               has_edit_comment = 'TICKET_EDIT_COMMENT' in perm(ticket.resource);"
       py:strip="">
-  <xi:include href="ticket_box.html" py:with="can_append = 'TICKET_APPEND' in perm(ticket.resource)"/>
-  <xi:include href="ticket_change.html" py:with="change = change_preview"/>
+  <xi:include href="ticket_box.html"/>
+  <div id="new-changes">
+    <xi:include href="ticket_changes.html"
+                py:with="changes = [c for c in changes if c.date > start_time];
+                         edited_comment=None; cnum_edit=0"/>
+  </div>
+  <input type="hidden" name="view_time" value="${to_utimestamp(ticket['changetime'])}"/>
+  <div id="preview"><xi:include href="ticket_change.html" py:with="change = change_preview"/></div>
 </html>
diff --git a/trac/ticket/web_ui.py b/trac/ticket/web_ui.py
--- a/trac/ticket/web_ui.py
+++ b/trac/ticket/web_ui.py
@@ -493,7 +493,7 @@ class TicketModule(Component):
             data.update({'action': None,
                          'reassign_owner': req.authname,
                          'resolve_resolution': None,
-                         'timestamp': str(ticket['changetime'])})
+                         'start_time': ticket['changetime']})
         elif req.method == 'POST': # 'Preview' or 'Submit'
             if 'cancel_comment' in req.args:
                 req.redirect(req.href.ticket(ticket.id))
@@ -556,9 +556,9 @@ class TicketModule(Component):
                 req.args['preview'] = True
 
             # Preview an existing ticket (after a Preview or a failed Save)
+            start_time = from_utimestamp(long(req.args.get('start_time', 0)))
             data.update({
-                'action': action,
-                'timestamp': req.args.get('ts'),
+                'action': action, 'start_time': start_time,
                 'reassign_owner': (req.args.get('reassign_choice') 
                                    or req.authname),
                 'resolve_resolution': req.args.get('resolve_choice'),
@@ -570,7 +570,7 @@ class TicketModule(Component):
                          'reassign_owner': req.authname,
                          'resolve_resolution': None,
                          # Store a timestamp for detecting "mid air collisions"
-                         'timestamp': str(ticket['changetime'])})
+                         'start_time': ticket['changetime']})
 
         data.update({'comment': req.args.get('comment'),
                      'cnum_edit': req.args.get('cnum_edit'),
@@ -655,7 +655,7 @@ class TicketModule(Component):
         return 'ticket.html', data, None
 
     def _prepare_data(self, req, ticket, absurls=False):
-        return {'ticket': ticket,
+        return {'ticket': ticket, 'to_utimestamp': to_utimestamp,
                 'context': web_context(req, ticket.resource,
                                                 absurls=absurls),
                 'preserve_newlines': self.must_preserve_newlines}
@@ -1141,7 +1141,8 @@ class TicketModule(Component):
 
         # Mid air collision?
         if ticket.exists and (ticket._old or comment or force_collision_check):
-            if req.args.get('ts') != str(ticket['changetime']):
+            changetime = ticket['changetime']
+            if req.args.get('view_time') != str(to_utimestamp(changetime)):
                 add_warning(req, _("Sorry, can not save your changes. "
                               "This ticket has been modified by someone else "
                               "since you started"))
@@ -1187,8 +1188,6 @@ class TicketModule(Component):
 
         # Validate comment numbering
         try:
-            # comment index must be a number
-            int(req.args.get('cnum') or 0)
             # replyto must be 'description' or a number
             replyto = req.args.get('replyto')
             if replyto != 'description':
@@ -1239,12 +1238,6 @@ class TicketModule(Component):
         req.redirect(req.href.ticket(ticket.id))
 
     def _do_save(self, req, ticket, action):
-        cnum = req.args.get('cnum')
-        replyto = req.args.get('replyto')
-        internal_cnum = cnum
-        if cnum and replyto: # record parent.child relationship
-            internal_cnum = '%s.%s' % (replyto, cnum)
-
         # Save the action controllers we need to call side-effects for before
         # we save the changes to the ticket.
         controllers = list(self._get_action_controllers(req, ticket, action))
@@ -1253,10 +1246,11 @@ class TicketModule(Component):
 
         fragment = ''
         now = datetime.now(utc)
-        if ticket.save_changes(get_reporter_id(req, 'author'),
-                                     req.args.get('comment'), when=now,
-                                     cnum=internal_cnum):
-            fragment = '#comment:' + cnum if cnum else ''
+        cnum = ticket.save_changes(get_reporter_id(req, 'author'),
+                                   req.args.get('comment'), when=now,
+                                   replyto=req.args.get('replyto'))
+        if cnum:
+            fragment = '#comment:%d' % cnum
             try:
                 tn = TicketNotifyEmail(self.env)
                 tn.notify(ticket, newticket=False, modtime=now)
@@ -1575,8 +1569,7 @@ class TicketModule(Component):
                                                             ticket[user])
         data.update({
             'context': context,
-            'fields': fields, 'changes': changes,
-            'replies': replies, 'cnum': cnum + 1,
+            'fields': fields, 'changes': changes, 'replies': replies,
             'attachments': AttachmentModule(self.env).attachment_data(context),
             'action_controls': action_controls,
             'action': selected_action,
diff --git a/tracopt/ticket/deleter.py b/tracopt/ticket/deleter.py
--- a/tracopt/ticket/deleter.py
+++ b/tracopt/ticket/deleter.py
@@ -54,6 +54,7 @@ class TicketDeleter(Component):
     # ITemplateStreamFilter methods
     
     def filter_stream(self, req, method, filename, stream, data):
+        # TODO: Also handle ticket_preview.html
         if filename != 'ticket.html':
             return stream
         ticket = data.get('ticket')
@@ -74,6 +75,7 @@ class TicketDeleter(Component):
         def delete_comment():
             for event in buffer:
                 cnum = event[1][1].get('id')[12:]
+                # FIXME: Identify comment by timestamp instead of cnum
                 return tag.form(
                     tag.div(
                         tag.input(type='hidden', name='action',

