Index: src/__phutil_library_map__.php
===================================================================
--- src/__phutil_library_map__.php
+++ src/__phutil_library_map__.php
@@ -1958,6 +1958,8 @@
     'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php',
     'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php',
     'PhrequentSearchEngine' => 'applications/phrequent/query/PhrequentSearchEngine.php',
+    'PhrequentTimeBlock' => 'applications/phrequent/storage/PhrequentTimeBlock.php',
+    'PhrequentTimeBlockTestCase' => 'applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php',
     'PhrequentTrackController' => 'applications/phrequent/controller/PhrequentTrackController.php',
     'PhrequentTrackableInterface' => 'applications/phrequent/interface/PhrequentTrackableInterface.php',
     'PhrequentUIEventListener' => 'applications/phrequent/event/PhrequentUIEventListener.php',
@@ -4230,6 +4232,8 @@
       1 => 'PhabricatorApplicationSearchResultsControllerInterface',
     ),
     'PhrequentSearchEngine' => 'PhabricatorApplicationSearchEngine',
+    'PhrequentTimeBlock' => 'Phobject',
+    'PhrequentTimeBlockTestCase' => 'PhabricatorTestCase',
     'PhrequentTrackController' => 'PhrequentController',
     'PhrequentUIEventListener' => 'PhutilEventListener',
     'PhrequentUserTime' =>
Index: src/applications/phrequent/event/PhrequentUIEventListener.php
===================================================================
--- src/applications/phrequent/event/PhrequentUIEventListener.php
+++ src/applications/phrequent/event/PhrequentUIEventListener.php
@@ -61,9 +61,9 @@
     $event->setValue('actions', $actions);
   }
 
-  private function handlePropertyEvent($event) {
-    $user = $event->getUser();
-    $object = $event->getValue('object');
+  private function handlePropertyEvent($ui_event) {
+    $user = $ui_event->getUser();
+    $object = $ui_event->getValue('object');
 
     if (!$object || !$object->getPHID()) {
       // No object, or the object has no PHID yet..
@@ -75,51 +75,64 @@
       return;
     }
 
-    $depth = false;
+    $events = id(new PhrequentUserTimeQuery())
+      ->setViewer($user)
+      ->withObjectPHIDs(array($object->getPHID()))
+      ->needPreemptingEvents(true)
+      ->execute();
+    $event_groups = mgroup($events, 'getUserPHID');
 
-    $stack = PhrequentUserTimeQuery::loadUserStack($user);
-    if ($stack) {
-      $stack = array_values($stack);
-      for ($ii = 0; $ii < count($stack); $ii++) {
-        if ($stack[$ii]->getObjectPHID() == $object->getPHID()) {
-          $depth = ($ii + 1);
-          break;
-        }
-      }
+    if (!$events) {
+      return;
     }
 
-    $time_spent = PhrequentUserTimeQuery::getTotalTimeSpentOnObject(
-      $object->getPHID());
+    $handles = id(new PhabricatorHandleQuery())
+      ->setViewer($user)
+      ->withPHIDs(array_keys($event_groups))
+      ->execute();
+
+    $status_view = new PHUIStatusListView();
+
+    foreach ($event_groups as $user_phid => $event_group) {
+      $item = new PHUIStatusItemView();
+      $item->setTarget($handles[$user_phid]->renderLink());
+
+      $state = 'stopped';
+      foreach ($event_group as $event) {
+        if ($event->getDateEnded() === null) {
+          if ($event->isPreempted()) {
+            $state = 'suspended';
+          } else {
+            $state = 'active';
+            break;
+          }
+        }
+      }
 
-    if (!$depth && !$time_spent) {
-      return;
-    }
+      switch ($state) {
+        case 'active':
+          $item->setIcon('time-green', pht('Working Now'));
+          break;
+        case 'suspended':
+          $item->setIcon('time-yellow', pht('Interrupted'));
+          break;
+        case 'stopped':
+          $item->setIcon('time-orange', pht('Not Working Now'));
+          break;
+      }
 
-    require_celerity_resource('phrequent-css');
-
-    $property = array();
-    if ($depth == 1) {
-      $property[] = phutil_tag(
-        'div',
-        array(
-          'class' => 'phrequent-tracking-property phrequent-active',
-        ),
-        pht('Currently Tracking'));
-    } else if ($depth > 1) {
-      $property[] = phutil_tag(
-        'div',
-        array(
-          'class' => 'phrequent-tracking-property phrequent-on-stack',
-        ),
-        pht('On Stack'));
-    }
+      $block = new PhrequentTimeBlock($event_group);
+      $item->setNote(
+        phabricator_format_relative_time(
+          $block->getTimeSpentOnObject(
+            $object->getPHID(),
+            time())));
 
-    if ($time_spent) {
-      $property[] = phabricator_format_relative_time_detailed($time_spent);
+      $status_view->addItem($item);
     }
 
-    $view = $event->getValue('view');
-    $view->addProperty(pht('Time Spent'), $property);
+    $view = $ui_event->getValue('view');
+    $view->addProperty(pht('Time Spent'), $status_view);
   }
 
 }
Index: src/applications/phrequent/query/PhrequentUserTimeQuery.php
===================================================================
--- src/applications/phrequent/query/PhrequentUserTimeQuery.php
+++ src/applications/phrequent/query/PhrequentUserTimeQuery.php
@@ -21,6 +21,8 @@
   private $order = self::ORDER_ID_ASC;
   private $ended = self::ENDED_ALL;
 
+  private $needPreemptingEvents;
+
   public function withUserPHIDs($user_phids) {
     $this->userPHIDs = $user_phids;
     return $this;
@@ -41,6 +43,11 @@
     return $this;
   }
 
+  public function needPreemptingEvents($need_events) {
+    $this->needPreemptingEvents = $need_events;
+    return $this;
+  }
+
   private function buildWhereClause(AphrontDatabaseConnection $conn) {
     $where = array();
 
@@ -150,6 +157,61 @@
     return $usertime->loadAllFromArray($data);
   }
 
+  protected function didFilterPage(array $page) {
+    if ($this->needPreemptingEvents) {
+      $usertime = new PhrequentUserTime();
+      $conn_r = $usertime->establishConnection('r');
+
+      $preempt = array();
+      foreach ($page as $event) {
+        $preempt[] = qsprintf(
+          $conn_r,
+          '(userPHID = %s AND
+            (dateStarted BETWEEN %d AND %d) AND
+            (dateEnded IS NULL OR dateEnded > %d))',
+          $event->getUserPHID(),
+          $event->getDateStarted(),
+          nonempty($event->getDateEnded(), PhabricatorTime::getNow()),
+          $event->getDateStarted());
+      }
+
+      $preempting_events = queryfx_all(
+        $conn_r,
+        'SELECT * FROM %T WHERE %Q ORDER BY dateStarted ASC, id ASC',
+        $usertime->getTableName(),
+        implode(' OR ', $preempt));
+      $preempting_events = $usertime->loadAllFromArray($preempting_events);
+
+      $preempting_events = mgroup($preempting_events, 'getUserPHID');
+
+      foreach ($page as $event) {
+        $e_start = $event->getDateStarted();
+        $e_end = $event->getDateEnded();
+
+        $select = array();
+        $user_events = idx($preempting_events, $event->getUserPHID(), array());
+        foreach ($user_events as $u_event) {
+          if ($u_event->getID() == $event->getID()) {
+            // Don't allow an event to preempt itself.
+            continue;
+          }
+
+          $u_start = $u_event->getDateStarted();
+          $u_end = $u_event->getDateEnded();
+
+          if (($u_start >= $e_start) && ($u_end <= $e_end) &&
+              ($u_end === null || $u_end > $e_start)) {
+            $select[] = $u_event;
+          }
+        }
+
+        $event->attachPreemptingEvents($select);
+      }
+    }
+
+    return $page;
+  }
+
 /* -(  Helper Functions ) --------------------------------------------------- */
 
   public static function getEndedSearchOptions() {
@@ -204,46 +266,6 @@
     return $count['N'] > 0;
   }
 
-  public static function loadUserStack(PhabricatorUser $user) {
-    if (!$user->isLoggedIn()) {
-      return array();
-    }
-
-    return id(new PhrequentUserTime())->loadAllWhere(
-      'userPHID = %s AND dateEnded IS NULL
-        ORDER BY dateStarted DESC, id DESC',
-      $user->getPHID());
-  }
-
-  public static function getTotalTimeSpentOnObject($phid) {
-    $usertime_dao = new PhrequentUserTime();
-    $conn = $usertime_dao->establishConnection('r');
-
-    // First calculate all the time spent where the
-    // usertime blocks have ended.
-    $sum_ended = queryfx_one(
-      $conn,
-      'SELECT SUM(usertime.dateEnded - usertime.dateStarted) N '.
-      'FROM %T usertime '.
-      'WHERE usertime.objectPHID = %s '.
-      'AND usertime.dateEnded IS NOT NULL',
-      $usertime_dao->getTableName(),
-      $phid);
-
-    // Now calculate the time spent where the usertime
-    // blocks have not yet ended.
-    $sum_not_ended = queryfx_one(
-      $conn,
-      'SELECT SUM(UNIX_TIMESTAMP() - usertime.dateStarted) N '.
-      'FROM %T usertime '.
-      'WHERE usertime.objectPHID = %s '.
-      'AND usertime.dateEnded IS NULL',
-      $usertime_dao->getTableName(),
-      $phid);
-
-    return $sum_ended['N'] + $sum_not_ended['N'];
-  }
-
   public static function getUserTimeSpentOnObject(
     PhabricatorUser $user,
     $phid) {
Index: src/applications/phrequent/storage/PhrequentTimeBlock.php
===================================================================
--- /dev/null
+++ src/applications/phrequent/storage/PhrequentTimeBlock.php
@@ -0,0 +1,155 @@
+<?php
+
+final class PhrequentTimeBlock extends Phobject {
+
+  private $events;
+
+  public function __construct(array $events) {
+    assert_instances_of($events, 'PhrequentUserTime');
+    $this->events = $events;
+  }
+
+  public function getTimeSpentOnObject($phid, $now) {
+    $ranges = idx($this->getObjectTimeRanges($now), $phid, array());
+
+    $sum = 0;
+    foreach ($ranges as $range) {
+      $sum += ($range[1] - $range[0]);
+    }
+
+    return $sum;
+  }
+
+  public function getObjectTimeRanges($now) {
+    $ranges = array();
+
+    $object_ranges = array();
+    foreach ($this->events as $event) {
+
+      // First, convert each event's preempting stack into a linear timeline
+      // of events.
+
+      $timeline = array();
+      $timeline[] = array(
+        'at' => $event->getDateStarted(),
+        'type' => 'start',
+      );
+      $timeline[] = array(
+        'at' => nonempty($event->getDateEnded(), $now),
+        'type' => 'end',
+      );
+
+      $base_phid = $event->getObjectPHID();
+
+      $preempts = $event->getPreemptingEvents();
+
+      foreach ($preempts as $preempt) {
+        $same_object = ($preempt->getObjectPHID() == $base_phid);
+        $timeline[] = array(
+          'at' => $preempt->getDateStarted(),
+          'type' => $same_object ? 'start' : 'push',
+        );
+        $timeline[] = array(
+          'at' => nonempty($preempt->getDateEnded(), $now),
+          'type' => $same_object ? 'end' : 'pop',
+        );
+      }
+
+      // Now, figure out how much time was actually spent working on the
+      // object.
+
+      $timeline = isort($timeline, 'at');
+
+      $stack = array();
+      $depth = null;
+
+      $ranges = array();
+      foreach ($timeline as $timeline_event) {
+        switch ($timeline_event['type']) {
+          case 'start':
+            $stack[] = $depth;
+            $depth = 0;
+            $range_start = $timeline_event['at'];
+            break;
+          case 'end':
+            if ($depth == 0) {
+              $ranges[] = array($range_start, $timeline_event['at']);
+            }
+            $depth = array_pop($stack);
+            break;
+          case 'push':
+            if ($depth == 0) {
+              $ranges[] = array($range_start, $timeline_event['at']);
+            }
+            $depth++;
+            break;
+          case 'pop':
+            $depth--;
+            if ($depth == 0) {
+              $range_start = $timeline_event['at'];
+            }
+            break;
+        }
+      }
+
+      $object_ranges[$base_phid][] = $ranges;
+    }
+
+    // Finally, collapse all the ranges so we don't double-count time.
+
+    foreach ($object_ranges as $phid => $ranges) {
+      $object_ranges[$phid] = self::mergeTimeRanges(array_mergev($ranges));
+    }
+
+    return $object_ranges;
+  }
+
+
+  /**
+   * Merge a list of time ranges (pairs of `<start, end>` epochs) so that no
+   * elements overlap. For example, the ranges:
+   *
+   *   array(
+   *     array(50, 150),
+   *     array(100, 175),
+   *   );
+   *
+   * ...are merged to:
+   *
+   *   array(
+   *     array(50, 175),
+   *   );
+   *
+   * This is used to avoid double-counting time on objects which had timers
+   * started multiple times.
+   *
+   * @param list<pair<int, int>> List of possibly overlapping time ranges.
+   * @return list<pair<int, int>> Nonoverlapping time ranges.
+   */
+  public static function mergeTimeRanges(array $ranges) {
+    $ranges = isort($ranges, 0);
+
+    $result = array();
+
+    $current = null;
+    foreach ($ranges as $key => $range) {
+      if ($current === null) {
+        $current = $range;
+        continue;
+      }
+
+      if ($range[0] <= $current[1]) {
+        $current[1] = max($range[1], $current[1]);
+        continue;
+      }
+
+      $result[] = $current;
+      $current = $range;
+    }
+
+    $result[] = $current;
+
+    return $result;
+  }
+
+}
Index: src/applications/phrequent/storage/PhrequentUserTime.php
===================================================================
--- src/applications/phrequent/storage/PhrequentUserTime.php
+++ src/applications/phrequent/storage/PhrequentUserTime.php
@@ -1,8 +1,5 @@
 <?php
 
-/**
- * @group phrequent
- */
 final class PhrequentUserTime extends PhrequentDAO
   implements PhabricatorPolicyInterface {
 
@@ -12,6 +9,8 @@
   protected $dateStarted;
   protected $dateEnded;
 
+  private $preemptingEvents = self::ATTACHABLE;
+
   public function getCapabilities() {
     return array(
       PhabricatorPolicyCapability::CAN_VIEW,
@@ -23,8 +22,11 @@
 
     switch ($capability) {
       case PhabricatorPolicyCapability::CAN_VIEW:
-        $policy = PhabricatorPolicies::POLICY_USER;
-        break;
+        // Since it's impossible to perform any meaningful computations with
+        // time if a user can't view some of it, visibility on tracked time is
+        // unrestricted. If we eventually lock it down, it should be per-user.
+        // (This doesn't mean that users can see tracked objects.)
+        return PhabricatorPolicies::getMostOpenPolicy();
     }
 
     return $policy;
@@ -36,8 +38,29 @@
 
 
   public function describeAutomaticCapability($capability) {
-    return pht(
-      'The user who tracked time can always view it.');
+    return null;
+  }
+
+  public function attachPreemptingEvents(array $events) {
+    $this->preemptingEvents = $events;
+    return $this;
+  }
+
+  public function getPreemptingEvents() {
+    return $this->assertAttached($this->preemptingEvents);
+  }
+
+  public function isPreempted() {
+    if ($this->getDateEnded() !== null) {
+      return false;
+    }
+    foreach ($this->getPreemptingEvents() as $event) {
+      if ($event->getDateEnded() === null &&
+          $event->getObjectPHID() != $this->getObjectPHID()) {
+        return true;
+      }
+    }
+    return false;
   }
 
 }
Index: src/applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php
===================================================================
--- /dev/null
+++ src/applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php
@@ -0,0 +1,128 @@
+<?php
+
+final class PhrequentTimeBlockTestCase extends PhabricatorTestCase {
+
+  public function testMergeTimeRanges() {
+
+    // Overlapping ranges.
+    $input = array(
+      array(50, 150),
+      array(100, 175),
+    );
+    $expect = array(
+      array(50, 175),
+    );
+
+    $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input));
+
+
+    // Identical ranges.
+    $input = array(
+      array(1, 1),
+      array(1, 1),
+      array(1, 1),
+    );
+    $expect = array(
+      array(1, 1),
+    );
+
+    $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input));
+
+
+    // Range which is a strict subset of another range.
+    $input = array(
+      array(2, 7),
+      array(1, 10),
+    );
+    $expect = array(
+      array(1, 10),
+    );
+
+    $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input));
+
+
+    // These are discontinuous and should not be merged.
+    $input = array(
+      array(5, 6),
+      array(7, 8),
+    );
+    $expect = array(
+      array(5, 6),
+      array(7, 8),
+    );
+
+    $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input));
+
+
+    // These overlap only on an edge, but should merge.
+    $input = array(
+      array(5, 6),
+      array(6, 7),
+    );
+    $expect = array(
+      array(5, 7),
+    );
+
+    $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input));
+  }
+
+  public function testPreemptingEvents() {
+
+    // Roughly, this is: got into work, started T1, had a meeting from 10-11,
+    // left the office around 2 to meet a client at a coffee shop, worked on
+    // T1 again for about 40 minutes, had the meeting from 3-3:30, finished up
+    // on T1, headed back to the office, got another hour of work in, and
+    // headed home.
+
+    $event = $this->newEvent('T1', 900, 1700);
+
+    $event->attachPreemptingEvents(
+      array(
+        $this->newEvent('meeting', 1000, 1100),
+        $this->newEvent('offsite', 1400, 1600),
+        $this->newEvent('T1', 1420, 1580),
+        $this->newEvent('offsite meeting', 1500, 1550),
+      ));
+
+    $block = new PhrequentTimeBlock(array($event));
+
+    $ranges = $block->getObjectTimeRanges(1800);
+    $this->assertEqual(
+      array(
+        'T1' => array(
+          array(900, 1000),  // Before morning meeting.
+          array(1100, 1400), // After morning meeting.
+          array(1420, 1500), // Coffee, before client meeting.
+          array(1550, 1580), // Coffee, after client meeting.
+          array(1600, 1700), // After returning from off site.
+        ),
+      ),
+      $ranges);
+
+
+    $event = $this->newEvent('T2', 100, 300);
+    $event->attachPreemptingEvents(
+      array(
+        $this->newEvent('meeting', 200, null),
+      ));
+
+    $block = new PhrequentTimeBlock(array($event));
+
+    $ranges = $block->getObjectTimeRanges(1000);
+    $this->assertEqual(
+      array(
+        'T2' => array(
+          array(100, 200),
+        ),
+      ),
+      $ranges);
+  }
+
+  private function newEvent($object_phid, $start_time, $end_time) {
+    return id(new PhrequentUserTime())
+      ->setObjectPHID($object_phid)
+      ->setDateStarted($start_time)
+      ->setDateEnded($end_time);
+  }
+
+}