@state = {}
     @hidden_labels = hidden_labels
 
-    earliest = nil
-    latest = nil
+    earliest, latest = nil, nil
     latest_date = nil
     @thread.each do |m, d, p|
       next unless m
       earliest ||= m
-      @state[m] = 
-        if m.has_label?(:unread) && m == earliest
-          :detailed
-        elsif m.has_label?(:starred) || m.has_label?(:unread)
-          :open
-        else
-          :closed
-        end
+      @state[m] = initial_state_for m
       if latest_date.nil? || m.date > latest_date
         latest_date = m.date
         latest = m
       end
     end
+
     @state[latest] = :open if @state[latest] == :closed
+    @state[earliest] = :detailed if earliest.has_label?(:unread)
 
     BufferManager.say "Loading message bodies..." do
       regen_chunks
     end
   end
 
-  ## not sure if this is really necessary but we might as well...
+  ## kinda slow for large threads. TODO: make faster
   def cleanup
-    @thread.each do |m, d, p|
-      if m && m.has_label?(:unread)
-        m.remove_label :unread 
-        UpdateManager.relay :read, m
+    BufferManager.say "Marking messages as read..." do
+      @thread.each do |m, d, p|
+        if m && m.has_label?(:unread)
+          m.remove_label :unread 
+          UpdateManager.relay :read, m
+        end
       end
     end
     @messages = @chunks = @text = nil
 
 private 
 
+  def initial_state_for m
+    if m.has_label?(:starred) || m.has_label?(:unread)
+      :open
+    else
+      :closed
+    end
+  end
+
   def update
     regen_text
     buffer.mark_dirty if buffer
 
     prev_m = nil
     @thread.each do |m, depth, parent|
+      ## we're occasionally called on @threads that have had messages
+      ## added to them since initialization. luckily we regen_text on
+      ## the entire thread every time the user does anything besides
+      ## scrolling (basically), so we can just slap this on here.
+      ##
+      ## to pick nits, the niceness that i do in the constructor with
+      ## 'latest' might not be valid, but i don't see that as a huge
+      ## issue.
+      @state[m] ||= initial_state_for m if m
+
       text = chunk_to_lines m, @state[m], @text.length, depth, parent
       (0 ... text.length).each do |i|
         @chunk_lines[@text.length + i] = m
     when Message
       message_patina_lines(chunk, state, parent, prefix) +
         (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
-
     when Message::Attachment
       [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]
     when Message::Text
 
 
   attr_reader :containers
   def initialize
-    raise "wrong thread, buddy!" if block_given?
+    ## ah, the joys of a multithreaded application with a class called
+    ## "Thread". i keep instantiating the wrong one...
+    raise "wrong Thread class, buddy!" if block_given?
     @containers = []
   end
 
     puts "=== end thread ==="
   end
 
-  ## yields each message and some stuff
+  ## yields each message, its depth, and its parent
+  ## note that the message can be a Message object, or :fake_root,
+  ## or nil.
   def each fake_root=false
     adj = 0
     root = @containers.find_all { |c| !Message.subj_is_reply?(c) }.argmin { |c| c.date }
   def set_labels l; each { |m, *o| m && m.labels = l }; end
   
   def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end
+  def dirty?; any? { |m, *o| m && m.dirty? }; end
   def save index; each { |m, *o| m && m.save(index) }; end
 
   def direct_participants