← Back to task

Commit a16de8c9

commit a16de8c9884f7eab639d1e5b016c9d6846866e03
Author: Ben Sima <ben@bensima.com>
Date:   Mon Dec 1 12:01:56 2025

    Remove separate Agent Log page, consolidate timeline styles
    
    - Rename agentLogScrollScript to timelineScrollScript - Target
    .timeline-events instead of obsolete .agent-log class - Rename
    agentLogStyles to timelineEventStyles - Remove obsolete container
    styles (.agent-log-section, .agent-log-live, .agent-log) - Remove
    dark mode styles for obsolete classes
    
    Task-Id: t-213.6

diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index f7a22193..2fc20646 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -1556,14 +1556,6 @@ instance Lucid.ToHtml TaskDetailPage where
                 Lucid.div_ [Lucid.class_ "detail-section"] <| do
                   Lucid.toHtml (DescriptionViewPartial (TaskCore.taskId task) (TaskCore.taskDescription task) (TaskCore.taskType task == TaskCore.Epic))
 
-                let comments = TaskCore.taskComments task
-                Lucid.div_ [Lucid.class_ "detail-section comments-section"] <| do
-                  Lucid.h3_ (Lucid.toHtml ("Comments (" <> tshow (length comments) <> ")"))
-                  if null comments
-                    then Lucid.p_ [Lucid.class_ "empty-msg"] "No comments yet."
-                    else traverse_ (renderComment now) comments
-                  commentForm (TaskCore.taskId task)
-
                 let children = filter (maybe False (TaskCore.matchesId (TaskCore.taskId task)) <. TaskCore.taskParent) allTasks
                 unless (null children) <| do
                   Lucid.div_ [Lucid.class_ "detail-section"] <| do
@@ -1592,12 +1584,6 @@ instance Lucid.ToHtml TaskDetailPage where
                   Lucid.h3_ "Execution Details"
                   renderExecutionDetails (TaskCore.taskId task) activities maybeRetry
 
-              when (TaskCore.taskStatus task == TaskCore.InProgress && not (null activities)) <| do
-                Lucid.div_ [Lucid.class_ "activity-section"] <| do
-                  Lucid.h3_ "Activity Timeline"
-                  Lucid.div_ [Lucid.class_ "activity-timeline"] <| do
-                    traverse_ renderActivity activities
-
               when (TaskCore.taskStatus task == TaskCore.Review) <| do
                 Lucid.div_ [Lucid.class_ "review-link-section"] <| do
                   Lucid.a_
@@ -1606,7 +1592,7 @@ instance Lucid.ToHtml TaskDetailPage where
                     ]
                     "Review This Task"
 
-              renderAgentLogSection (TaskCore.taskId task) agentEvents (TaskCore.taskStatus task) now
+              renderUnifiedTimeline (TaskCore.taskId task) (TaskCore.taskComments task) agentEvents (TaskCore.taskStatus task) now
     where
       renderDependency :: (Monad m) => TaskCore.Dependency -> Lucid.HtmlT m ()
       renderDependency dep =
@@ -1621,40 +1607,6 @@ instance Lucid.ToHtml TaskDetailPage where
           Lucid.span_ [Lucid.class_ "child-title"] <| Lucid.toHtml (" - " <> TaskCore.taskTitle child)
           Lucid.span_ [Lucid.class_ "child-status"] <| Lucid.toHtml (" [" <> tshow (TaskCore.taskStatus child) <> "]")
 
-      renderComment :: (Monad m) => UTCTime -> TaskCore.Comment -> Lucid.HtmlT m ()
-      renderComment currentTime c =
-        Lucid.div_ [Lucid.class_ "comment-card"] <| do
-          Lucid.div_ [Lucid.class_ "comment-text markdown-content"] (renderMarkdown (TaskCore.commentText c))
-          Lucid.div_ [Lucid.class_ "comment-meta"] <| do
-            Lucid.span_ [Lucid.class_ ("comment-author " <> authorClass)] (Lucid.toHtml (authorLabel (TaskCore.commentAuthor c)))
-            Lucid.span_ [Lucid.class_ "comment-time"] (renderRelativeTimestamp currentTime (TaskCore.commentCreatedAt c))
-        where
-          authorClass = case TaskCore.commentAuthor c of
-            TaskCore.Human -> "author-human"
-            TaskCore.Junior -> "author-junior"
-            TaskCore.System -> "author-system"
-          authorLabel author = case author of
-            TaskCore.Human -> "Human" :: Text
-            TaskCore.Junior -> "Junior" :: Text
-            TaskCore.System -> "System" :: Text
-
-      commentForm :: (Monad m) => Text -> Lucid.HtmlT m ()
-      commentForm tid =
-        Lucid.form_
-          [ Lucid.method_ "POST",
-            Lucid.action_ ("/tasks/" <> tid <> "/comment"),
-            Lucid.class_ "comment-form"
-          ]
-          <| do
-            Lucid.textarea_
-              [ Lucid.name_ "comment",
-                Lucid.placeholder_ "Add a comment...",
-                Lucid.rows_ "3",
-                Lucid.class_ "comment-textarea"
-              ]
-              ""
-            Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-primary"] "Post Comment"
-
       renderCommit :: (Monad m) => Text -> GitCommit -> Lucid.HtmlT m ()
       renderCommit tid c =
         Lucid.div_ [Lucid.class_ "commit-item"] <| do
@@ -1670,42 +1622,6 @@ instance Lucid.ToHtml TaskDetailPage where
             Lucid.span_ [Lucid.class_ "commit-date"] (Lucid.toHtml (commitRelativeDate c))
             Lucid.span_ [Lucid.class_ "commit-files"] (Lucid.toHtml (tshow (commitFilesChanged c) <> " files"))
 
-      renderActivity :: (Monad m) => TaskCore.TaskActivity -> Lucid.HtmlT m ()
-      renderActivity act =
-        Lucid.div_ [Lucid.class_ ("activity-item " <> stageClass (TaskCore.activityStage act))] <| do
-          Lucid.div_ [Lucid.class_ "activity-icon"] (stageIcon (TaskCore.activityStage act))
-          Lucid.div_ [Lucid.class_ "activity-content"] <| do
-            Lucid.div_ [Lucid.class_ "activity-header"] <| do
-              Lucid.span_ [Lucid.class_ "activity-stage"] (Lucid.toHtml (tshow (TaskCore.activityStage act)))
-              Lucid.span_ [Lucid.class_ "activity-time"] (renderRelativeTimestamp now (TaskCore.activityTimestamp act))
-            case TaskCore.activityMessage act of
-              Nothing -> pure ()
-              Just msg -> Lucid.p_ [Lucid.class_ "activity-message"] (Lucid.toHtml msg)
-            case TaskCore.activityMetadata act of
-              Nothing -> pure ()
-              Just meta ->
-                Lucid.details_ [Lucid.class_ "activity-metadata"] <| do
-                  Lucid.summary_ "Metadata"
-                  Lucid.pre_ [Lucid.class_ "metadata-json"] (Lucid.toHtml meta)
-
-      stageClass :: TaskCore.ActivityStage -> Text
-      stageClass stage = case stage of
-        TaskCore.Claiming -> "stage-claiming"
-        TaskCore.Running -> "stage-running"
-        TaskCore.Reviewing -> "stage-reviewing"
-        TaskCore.Retrying -> "stage-retrying"
-        TaskCore.Completed -> "stage-completed"
-        TaskCore.Failed -> "stage-failed"
-
-      stageIcon :: (Monad m) => TaskCore.ActivityStage -> Lucid.HtmlT m ()
-      stageIcon stage = case stage of
-        TaskCore.Claiming -> "●"
-        TaskCore.Running -> "▶"
-        TaskCore.Reviewing -> "◎"
-        TaskCore.Retrying -> "↻"
-        TaskCore.Completed -> "✓"
-        TaskCore.Failed -> "✗"
-
       renderExecutionDetails :: (Monad m) => Text -> [TaskCore.TaskActivity] -> Maybe TaskCore.RetryContext -> Lucid.HtmlT m ()
       renderExecutionDetails _ acts retryCtx =
         let runningActs = filter (\a -> TaskCore.activityStage a == TaskCore.Running) acts
@@ -2416,109 +2332,245 @@ renderInlinePart part = case part of
   InlineCode txt -> Lucid.code_ [Lucid.class_ "md-inline-code"] (Lucid.toHtml txt)
   BoldText txt -> Lucid.strong_ (Lucid.toHtml txt)
 
-renderAgentLogSection :: (Monad m) => Text -> [TaskCore.StoredEvent] -> TaskCore.Status -> UTCTime -> Lucid.HtmlT m ()
-renderAgentLogSection tid events status now = do
-  let shouldShow = not (null events) || status == TaskCore.InProgress
-  when shouldShow <| do
-    let isInProgress = status == TaskCore.InProgress
-        pollAttrs =
-          if isInProgress
-            then
-              [ Lucid.makeAttribute "hx-get" ("/partials/task/" <> tid <> "/events"),
-                Lucid.makeAttribute "hx-trigger" "every 3s",
-                Lucid.makeAttribute "hx-swap" "innerHTML",
-                Lucid.makeAttribute "hx-on::before-request" "var log = this.querySelector('.agent-log'); if(log) this.dataset.scroll = log.scrollTop",
-                Lucid.makeAttribute "hx-on::after-swap" "var log = this.querySelector('.agent-log'); if(log && this.dataset.scroll) log.scrollTop = this.dataset.scroll"
-              ]
-            else []
-    Lucid.div_ ([Lucid.class_ "agent-log-section", Lucid.id_ "agent-log-container"] <> pollAttrs) <| do
-      Lucid.h3_ <| do
-        Lucid.toHtml ("Agent Log (" <> tshow (length events) <> ")")
-        when isInProgress <| Lucid.span_ [Lucid.class_ "agent-log-live"] " LIVE"
-      if null events
-        then Lucid.p_ [Lucid.class_ "empty-msg"] "Agent session starting..."
-        else do
-          Lucid.div_ [Lucid.class_ "agent-log"] <| do
-            traverse_ (renderAgentEvent now) events
-          agentLogScrollScript
+-- | Comment form for adding new comments
+commentForm :: (Monad m) => Text -> Lucid.HtmlT m ()
+commentForm tid =
+  Lucid.form_
+    [ Lucid.method_ "POST",
+      Lucid.action_ ("/tasks/" <> tid <> "/comment"),
+      Lucid.class_ "comment-form"
+    ]
+    <| do
+      Lucid.textarea_
+        [ Lucid.name_ "comment",
+          Lucid.placeholder_ "Add a comment...",
+          Lucid.rows_ "3",
+          Lucid.class_ "comment-textarea"
+        ]
+        ""
+      Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-primary"] "Post Comment"
+
+-- | Unified timeline view combining comments, status changes, and agent events
+renderUnifiedTimeline :: (Monad m) => Text -> [TaskCore.Comment] -> [TaskCore.StoredEvent] -> TaskCore.Status -> UTCTime -> Lucid.HtmlT m ()
+renderUnifiedTimeline tid legacyComments events status now = do
+  let isInProgress = status == TaskCore.InProgress
+      pollAttrs =
+        if isInProgress
+          then
+            [ Lucid.makeAttribute "hx-get" ("/partials/task/" <> tid <> "/events"),
+              Lucid.makeAttribute "hx-trigger" "every 3s",
+              Lucid.makeAttribute "hx-swap" "innerHTML",
+              Lucid.makeAttribute "hx-on::before-request" "var log = this.querySelector('.timeline-events'); if(log) this.dataset.scroll = log.scrollTop",
+              Lucid.makeAttribute "hx-on::after-swap" "var log = this.querySelector('.timeline-events'); if(log && this.dataset.scroll) log.scrollTop = this.dataset.scroll"
+            ]
+          else []
+  Lucid.div_ ([Lucid.class_ "unified-timeline-section", Lucid.id_ "unified-timeline"] <> pollAttrs) <| do
+    Lucid.h3_ <| do
+      Lucid.toHtml ("Timeline (" <> tshow (length events + length legacyComments) <> ")")
+      when isInProgress <| Lucid.span_ [Lucid.class_ "timeline-live"] " LIVE"
+
+    if null events && null legacyComments
+      then Lucid.p_ [Lucid.class_ "empty-msg"] "No activity yet."
+      else do
+        Lucid.div_ [Lucid.class_ "timeline-events"] <| do
+          traverse_ (renderTimelineEvent now) events
+        when isInProgress <| timelineScrollScript
+
+    commentForm tid
 
-renderAgentEvent :: (Monad m) => UTCTime -> TaskCore.StoredEvent -> Lucid.HtmlT m ()
-renderAgentEvent now event =
+-- | Render a single timeline event with icon, actor label, and timestamp
+renderTimelineEvent :: (Monad m) => UTCTime -> TaskCore.StoredEvent -> Lucid.HtmlT m ()
+renderTimelineEvent now event =
   let eventType = TaskCore.storedEventType event
       content = TaskCore.storedEventContent event
       timestamp = TaskCore.storedEventTimestamp event
+      actor = TaskCore.storedEventActor event
       eventId = TaskCore.storedEventId event
+      (icon, label) = eventTypeIconAndLabel eventType
    in Lucid.div_
-        [ Lucid.class_ ("agent-event agent-event-" <> eventType),
+        [ Lucid.class_ ("timeline-event timeline-event-" <> eventType),
           Lucid.makeAttribute "data-event-id" (tshow eventId)
         ]
         <| do
           case eventType of
-            "Assistant" -> renderAssistantEvent content timestamp now
-            "ToolCall" -> renderToolCallEvent content timestamp now
-            "ToolResult" -> renderToolResultEvent content timestamp now
-            "Cost" -> renderCostEvent content
-            "Error" -> renderErrorEvent content timestamp now
-            "Complete" -> renderCompleteEvent timestamp now
-            _ -> Lucid.div_ [Lucid.class_ "event-unknown"] (Lucid.toHtml content)
-
-renderAssistantEvent :: (Monad m) => Text -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
-renderAssistantEvent content timestamp now =
-  Lucid.div_ [Lucid.class_ "event-assistant"] <| do
+            "comment" -> renderCommentTimelineEvent content actor timestamp now
+            "status_change" -> renderStatusChangeEvent content actor timestamp now
+            "claim" -> renderActivityEvent icon label content actor timestamp now
+            "running" -> renderActivityEvent icon label content actor timestamp now
+            "reviewing" -> renderActivityEvent icon label content actor timestamp now
+            "retrying" -> renderActivityEvent icon label content actor timestamp now
+            "complete" -> renderActivityEvent icon label content actor timestamp now
+            "error" -> renderErrorTimelineEvent content actor timestamp now
+            "Assistant" -> renderAssistantTimelineEvent content actor timestamp now
+            "ToolCall" -> renderToolCallTimelineEvent content actor timestamp now
+            "ToolResult" -> renderToolResultTimelineEvent content actor timestamp now
+            "Cost" -> renderCostTimelineEvent content
+            "Checkpoint" -> renderCheckpointEvent content actor timestamp now
+            "Guardrail" -> renderGuardrailEvent content actor timestamp now
+            _ -> renderGenericEvent eventType content actor timestamp now
+
+-- | Get icon and label for event type
+eventTypeIconAndLabel :: Text -> (Text, Text)
+eventTypeIconAndLabel "comment" = ("💬", "Comment")
+eventTypeIconAndLabel "status_change" = ("🔄", "Status")
+eventTypeIconAndLabel "claim" = ("🤖", "Claimed")
+eventTypeIconAndLabel "running" = ("▶️", "Running")
+eventTypeIconAndLabel "reviewing" = ("👀", "Reviewing")
+eventTypeIconAndLabel "retrying" = ("🔁", "Retrying")
+eventTypeIconAndLabel "complete" = ("✅", "Complete")
+eventTypeIconAndLabel "error" = ("❌", "Error")
+eventTypeIconAndLabel "Assistant" = ("💭", "Thought")
+eventTypeIconAndLabel "ToolCall" = ("🔧", "Tool")
+eventTypeIconAndLabel "ToolResult" = ("📄", "Result")
+eventTypeIconAndLabel "Cost" = ("💰", "Cost")
+eventTypeIconAndLabel "Checkpoint" = ("📍", "Checkpoint")
+eventTypeIconAndLabel "Guardrail" = ("⚠️", "Guardrail")
+eventTypeIconAndLabel t = ("📝", t)
+
+-- | Render actor label
+renderActorLabel :: (Monad m) => TaskCore.CommentAuthor -> Lucid.HtmlT m ()
+renderActorLabel actor =
+  let (cls, label) :: (Text, Text) = case actor of
+        TaskCore.Human -> ("actor-human", "human")
+        TaskCore.Junior -> ("actor-junior", "junior")
+        TaskCore.System -> ("actor-system", "system")
+   in Lucid.span_ [Lucid.class_ ("actor-label " <> cls)] (Lucid.toHtml ("[" <> label <> "]"))
+
+-- | Render comment event
+renderCommentTimelineEvent :: (Monad m) => Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderCommentTimelineEvent content actor timestamp now =
+  Lucid.div_ [Lucid.class_ "timeline-comment"] <| do
     Lucid.div_ [Lucid.class_ "event-header"] <| do
       Lucid.span_ [Lucid.class_ "event-icon"] "💬"
-      Lucid.span_ [Lucid.class_ "event-label"] "Assistant"
+      renderActorLabel actor
       renderRelativeTimestamp now timestamp
-    Lucid.div_ [Lucid.class_ "event-content event-bubble"] <| do
+    Lucid.div_ [Lucid.class_ "event-content comment-bubble"] <| do
+      Lucid.toHtml content
+
+-- | Render status change event
+renderStatusChangeEvent :: (Monad m) => Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderStatusChangeEvent content actor timestamp now =
+  Lucid.div_ [Lucid.class_ "timeline-status-change"] <| do
+    Lucid.span_ [Lucid.class_ "event-icon"] "🔄"
+    renderActorLabel actor
+    Lucid.span_ [Lucid.class_ "status-change-text"] (Lucid.toHtml (parseStatusChange content))
+    renderRelativeTimestamp now timestamp
+
+-- | Parse status change JSON
+parseStatusChange :: Text -> Text
+parseStatusChange content =
+  case Aeson.decode (LBS.fromStrict (str content)) of
+    Just (Aeson.Object obj) ->
+      let fromStatus = case KeyMap.lookup "from" obj of
+            Just (Aeson.String s) -> s
+            _ -> "?"
+          toStatus = case KeyMap.lookup "to" obj of
+            Just (Aeson.String s) -> s
+            _ -> "?"
+       in fromStatus <> " → " <> toStatus
+    _ -> content
+
+-- | Render activity event (claim, running, etc.)
+renderActivityEvent :: (Monad m) => Text -> Text -> Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderActivityEvent icon label content actor timestamp now =
+  Lucid.div_ [Lucid.class_ "timeline-activity"] <| do
+    Lucid.span_ [Lucid.class_ "event-icon"] (Lucid.toHtml icon)
+    Lucid.span_ [Lucid.class_ "event-label"] (Lucid.toHtml label)
+    renderActorLabel actor
+    unless (Text.null content) <| Lucid.span_ [Lucid.class_ "activity-detail"] (Lucid.toHtml content)
+    renderRelativeTimestamp now timestamp
+
+-- | Render error event
+renderErrorTimelineEvent :: (Monad m) => Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderErrorTimelineEvent content actor timestamp now =
+  Lucid.div_ [Lucid.class_ "timeline-error"] <| do
+    Lucid.div_ [Lucid.class_ "event-header"] <| do
+      Lucid.span_ [Lucid.class_ "event-icon"] "❌"
+      Lucid.span_ [Lucid.class_ "event-label"] "Error"
+      renderActorLabel actor
+      renderRelativeTimestamp now timestamp
+    Lucid.div_ [Lucid.class_ "event-content error-message"] (Lucid.toHtml content)
+
+-- | Render assistant thought event
+renderAssistantTimelineEvent :: (Monad m) => Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderAssistantTimelineEvent content _actor timestamp now =
+  Lucid.div_ [Lucid.class_ "timeline-thought"] <| do
+    Lucid.div_ [Lucid.class_ "event-header"] <| do
+      Lucid.span_ [Lucid.class_ "event-icon"] "💭"
+      Lucid.span_ [Lucid.class_ "event-label"] "Thought"
+      renderActorLabel TaskCore.Junior
+      renderRelativeTimestamp now timestamp
+    Lucid.div_ [Lucid.class_ "event-content thought-bubble"] <| do
       let truncated = Text.take 2000 content
           isTruncated = Text.length content > 2000
       renderTextWithNewlines truncated
       when isTruncated <| Lucid.span_ [Lucid.class_ "event-truncated"] "..."
 
-renderToolCallEvent :: (Monad m) => Text -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
-renderToolCallEvent content timestamp now =
+-- | Render tool call event
+renderToolCallTimelineEvent :: (Monad m) => Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderToolCallTimelineEvent content _actor timestamp now =
   let (toolName, args) = parseToolCallContent content
-   in Lucid.details_ [Lucid.class_ "event-tool-call"] <| do
+   in Lucid.details_ [Lucid.class_ "timeline-tool-call"] <| do
         Lucid.summary_ <| do
           Lucid.span_ [Lucid.class_ "event-icon"] "🔧"
-          Lucid.span_ [Lucid.class_ "event-label tool-name"] (Lucid.toHtml toolName)
+          Lucid.span_ [Lucid.class_ "tool-name"] (Lucid.toHtml toolName)
+          renderActorLabel TaskCore.Junior
           renderRelativeTimestamp now timestamp
         Lucid.div_ [Lucid.class_ "event-content tool-args"] <| do
           renderCollapsibleOutput args
 
-renderToolResultEvent :: (Monad m) => Text -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
-renderToolResultEvent content timestamp now =
+-- | Render tool result event (collapsed by default)
+renderToolResultTimelineEvent :: (Monad m) => Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderToolResultTimelineEvent content _actor timestamp now =
   let lineCount = length (Text.lines content)
-   in Lucid.details_ [Lucid.class_ "event-tool-result"] <| do
+   in Lucid.details_ [Lucid.class_ "timeline-tool-result"] <| do
         Lucid.summary_ <| do
-          Lucid.span_ [Lucid.class_ "event-icon"] "📋"
+          Lucid.span_ [Lucid.class_ "event-icon"] "📄"
           Lucid.span_ [Lucid.class_ "event-label"] "Result"
           when (lineCount > 1)
             <| Lucid.span_ [Lucid.class_ "line-count"] (Lucid.toHtml (tshow lineCount <> " lines"))
           renderRelativeTimestamp now timestamp
         Lucid.pre_ [Lucid.class_ "event-content tool-output"] (renderDecodedToolResult content)
 
-renderCostEvent :: (Monad m) => Text -> Lucid.HtmlT m ()
-renderCostEvent content =
-  Lucid.div_ [Lucid.class_ "event-cost"] <| do
+-- | Render cost event (inline)
+renderCostTimelineEvent :: (Monad m) => Text -> Lucid.HtmlT m ()
+renderCostTimelineEvent content =
+  Lucid.div_ [Lucid.class_ "timeline-cost"] <| do
     Lucid.span_ [Lucid.class_ "event-icon"] "💰"
     Lucid.span_ [Lucid.class_ "cost-text"] (Lucid.toHtml content)
 
-renderErrorEvent :: (Monad m) => Text -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
-renderErrorEvent content timestamp now =
-  Lucid.div_ [Lucid.class_ "event-error"] <| do
+-- | Render checkpoint event
+renderCheckpointEvent :: (Monad m) => Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderCheckpointEvent content actor timestamp now =
+  Lucid.div_ [Lucid.class_ "timeline-checkpoint"] <| do
     Lucid.div_ [Lucid.class_ "event-header"] <| do
-      Lucid.span_ [Lucid.class_ "event-icon"] "❌"
-      Lucid.span_ [Lucid.class_ "event-label"] "Error"
+      Lucid.span_ [Lucid.class_ "event-icon"] "📍"
+      Lucid.span_ [Lucid.class_ "event-label"] "Checkpoint"
+      renderActorLabel actor
       renderRelativeTimestamp now timestamp
-    Lucid.div_ [Lucid.class_ "event-content error-message"] (Lucid.toHtml content)
+    Lucid.div_ [Lucid.class_ "event-content checkpoint-content"] (Lucid.toHtml content)
 
-renderCompleteEvent :: (Monad m) => UTCTime -> UTCTime -> Lucid.HtmlT m ()
-renderCompleteEvent timestamp now =
-  Lucid.div_ [Lucid.class_ "event-complete"] <| do
-    Lucid.span_ [Lucid.class_ "event-icon"] "✅"
-    Lucid.span_ [Lucid.class_ "event-label"] "Session completed"
-    renderRelativeTimestamp now timestamp
+-- | Render guardrail event
+renderGuardrailEvent :: (Monad m) => Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderGuardrailEvent content actor timestamp now =
+  Lucid.div_ [Lucid.class_ "timeline-guardrail"] <| do
+    Lucid.div_ [Lucid.class_ "event-header"] <| do
+      Lucid.span_ [Lucid.class_ "event-icon"] "⚠️"
+      Lucid.span_ [Lucid.class_ "event-label"] "Guardrail"
+      renderActorLabel actor
+      renderRelativeTimestamp now timestamp
+    Lucid.div_ [Lucid.class_ "event-content guardrail-content"] (Lucid.toHtml content)
+
+-- | Render generic/unknown event
+renderGenericEvent :: (Monad m) => Text -> Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
+renderGenericEvent eventType content actor timestamp now =
+  Lucid.div_ [Lucid.class_ "timeline-generic"] <| do
+    Lucid.div_ [Lucid.class_ "event-header"] <| do
+      Lucid.span_ [Lucid.class_ "event-icon"] "📝"
+      Lucid.span_ [Lucid.class_ "event-label"] (Lucid.toHtml eventType)
+      renderActorLabel actor
+      renderRelativeTimestamp now timestamp
+    unless (Text.null content) <| Lucid.div_ [Lucid.class_ "event-content"] (Lucid.toHtml content)
 
 parseToolCallContent :: Text -> (Text, Text)
 parseToolCallContent content =
@@ -2556,14 +2608,14 @@ renderDecodedToolResult content =
         _ -> Lucid.toHtml content -- Fallback to raw if no output field
     _ -> Lucid.toHtml content -- Fallback to raw if not JSON
 
-agentLogScrollScript :: (Monad m) => Lucid.HtmlT m ()
-agentLogScrollScript =
+timelineScrollScript :: (Monad m) => Lucid.HtmlT m ()
+timelineScrollScript =
   Lucid.script_
     [ Lucid.type_ "text/javascript"
     ]
     ( Text.unlines
         [ "(function() {",
-          "  var log = document.querySelector('.agent-log');",
+          "  var log = document.querySelector('.timeline-events');",
           "  if (log) {",
           "    var isNearBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 100;",
           "    if (isNearBottom) {",
@@ -2578,14 +2630,14 @@ instance Lucid.ToHtml AgentEventsPartial where
   toHtmlRaw = Lucid.toHtml
   toHtml (AgentEventsPartial events isInProgress now) = do
     Lucid.h3_ <| do
-      Lucid.toHtml ("Agent Log (" <> tshow (length events) <> ")")
-      when isInProgress <| Lucid.span_ [Lucid.class_ "agent-log-live"] " LIVE"
+      Lucid.toHtml ("Timeline (" <> tshow (length events) <> ")")
+      when isInProgress <| Lucid.span_ [Lucid.class_ "timeline-live"] " LIVE"
     if null events
-      then Lucid.p_ [Lucid.class_ "empty-msg"] "Agent session starting..."
+      then Lucid.p_ [Lucid.class_ "empty-msg"] "No activity yet."
       else do
-        Lucid.div_ [Lucid.class_ "agent-log"] <| do
-          traverse_ (renderAgentEvent now) events
-        agentLogScrollScript
+        Lucid.div_ [Lucid.class_ "timeline-events"] <| do
+          traverse_ (renderTimelineEvent now) events
+        timelineScrollScript
 
 -- | Stream agent events as SSE
 streamAgentEvents :: Text -> Text -> IO (SourceIO ByteString)
@@ -2862,12 +2914,12 @@ server =
             if TaskCore.taskType task == TaskCore.Epic
               then Just </ liftIO (TaskCore.getAggregatedMetrics tid)
               else pure Nothing
-          agentEvents <- liftIO (TaskCore.getEventsForTask tid)
+          agentEvents <- liftIO (TaskCore.getAllEventsForTask tid)
           pure (TaskDetailFound task tasks activities retryCtx commits aggMetrics agentEvents now)
 
     taskStatusHandler :: Text -> StatusForm -> Servant.Handler StatusBadgePartial
     taskStatusHandler tid (StatusForm newStatus) = do
-      liftIO <| TaskCore.updateTaskStatus tid newStatus []
+      liftIO <| TaskCore.updateTaskStatusWithActor tid newStatus [] TaskCore.Human
       pure (StatusBadgePartial newStatus tid)
 
     taskPriorityHandler :: Text -> PriorityForm -> Servant.Handler PriorityBadgePartial
@@ -2928,7 +2980,7 @@ server =
     taskAcceptHandler tid = do
       liftIO <| do
         TaskCore.clearRetryContext tid
-        TaskCore.updateTaskStatus tid TaskCore.Done []
+        TaskCore.updateTaskStatusWithActor tid TaskCore.Done [] TaskCore.Human
       pure <| addHeader ("/tasks/" <> tid) NoContent
 
     taskRejectHandler :: Text -> RejectForm -> Servant.Handler (Headers '[Header "Location" Text] NoContent)
@@ -2951,14 +3003,14 @@ server =
               TaskCore.retryReason = accumulatedReason,
               TaskCore.retryNotes = maybeCtx +> TaskCore.retryNotes
             }
-        TaskCore.updateTaskStatus tid TaskCore.Open []
+        TaskCore.updateTaskStatusWithActor tid TaskCore.Open [] TaskCore.Human
       pure <| addHeader ("/tasks/" <> tid) NoContent
 
     taskResetRetriesHandler :: Text -> Servant.Handler (Headers '[Header "Location" Text] NoContent)
     taskResetRetriesHandler tid = do
       liftIO <| do
         TaskCore.clearRetryContext tid
-        TaskCore.updateTaskStatus tid TaskCore.Open []
+        TaskCore.updateTaskStatusWithActor tid TaskCore.Open [] TaskCore.Human
       pure <| addHeader ("/tasks/" <> tid) NoContent
 
     recentActivityNewHandler :: Maybe Int -> Servant.Handler RecentActivityNewPartial
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
index 1f11255b..a169cd7a 100644
--- a/Omni/Jr/Web/Style.hs
+++ b/Omni/Jr/Web/Style.hs
@@ -39,7 +39,8 @@ stylesheet = do
   taskMetaStyles
   timeFilterStyles
   sortDropdownStyles
-  agentLogStyles
+  timelineEventStyles
+  unifiedTimelineStyles
   responsiveStyles
   darkModeStyles
 
@@ -1422,31 +1423,8 @@ taskMetaStyles = do
     color "#d1d5db"
     Stylesheet.key "user-select" ("none" :: Text)
 
-agentLogStyles :: Css
-agentLogStyles = do
-  ".agent-log-section" ? do
-    marginTop (em 1)
-    paddingTop (em 1)
-    borderTop (px 1) solid "#e5e7eb"
-  ".agent-log-live" ? do
-    fontSize (px 10)
-    fontWeight bold
-    color "#10b981"
-    backgroundColor "#d1fae5"
-    padding (px 2) (px 6) (px 2) (px 6)
-    borderRadius (px 10) (px 10) (px 10) (px 10)
-    marginLeft (px 8)
-    textTransform uppercase
-    Stylesheet.key "animation" ("pulse 2s infinite" :: Text)
-  ".agent-log" ? do
-    maxHeight (px 600)
-    overflowY auto
-    display flex
-    flexDirection column
-    Stylesheet.key "gap" ("8px" :: Text)
-    padding (px 8) (px 0) (px 8) (px 0)
-  ".agent-event" ? do
-    fontSize (px 13)
+timelineEventStyles :: Css
+timelineEventStyles = do
   ".event-header" ? do
     display flex
     alignItems center
@@ -1567,6 +1545,154 @@ agentLogStyles = do
   ".output-collapsible" |> "summary" # hover ? textDecoration underline
   Stylesheet.key "@keyframes pulse" ("0%, 100% { opacity: 1; } 50% { opacity: 0.5; }" :: Text)
 
+unifiedTimelineStyles :: Css
+unifiedTimelineStyles = do
+  ".unified-timeline-section" ? do
+    marginTop (em 1.5)
+    paddingTop (em 1)
+    borderTop (px 1) solid "#e5e7eb"
+  ".timeline-live" ? do
+    fontSize (px 10)
+    fontWeight bold
+    color "#10b981"
+    backgroundColor "#d1fae5"
+    padding (px 2) (px 6) (px 2) (px 6)
+    borderRadius (px 10) (px 10) (px 10) (px 10)
+    marginLeft (px 8)
+    textTransform uppercase
+    Stylesheet.key "animation" ("pulse 2s infinite" :: Text)
+  ".timeline-events" ? do
+    maxHeight (px 600)
+    overflowY auto
+    display flex
+    flexDirection column
+    Stylesheet.key "gap" ("12px" :: Text)
+    padding (px 12) (px 0) (px 12) (px 0)
+  ".timeline-event" ? do
+    fontSize (px 13)
+    lineHeight (em 1.4)
+  ".actor-label" ? do
+    fontSize (px 11)
+    fontWeight (weight 500)
+    padding (px 1) (px 4) (px 1) (px 4)
+    borderRadius (px 3) (px 3) (px 3) (px 3)
+    marginLeft (px 4)
+    marginRight (px 4)
+  ".actor-human" ? do
+    color "#7c3aed"
+    backgroundColor "#f3e8ff"
+  ".actor-junior" ? do
+    color "#0369a1"
+    backgroundColor "#e0f2fe"
+  ".actor-system" ? do
+    color "#6b7280"
+    backgroundColor "#f3f4f6"
+  ".timeline-comment" ? do
+    paddingLeft (px 4)
+  ".timeline-comment" |> ".comment-bubble" ? do
+    backgroundColor "#f3f4f6"
+    padding (px 10) (px 14) (px 10) (px 14)
+    borderRadius (px 8) (px 8) (px 8) (px 8)
+    whiteSpace preWrap
+    marginTop (px 6)
+  ".timeline-status-change" ? do
+    display flex
+    alignItems center
+    Stylesheet.key "gap" ("6px" :: Text)
+    flexWrap Flexbox.wrap
+    padding (px 6) (px 8) (px 6) (px 8)
+    backgroundColor "#f0fdf4"
+    borderRadius (px 6) (px 6) (px 6) (px 6)
+    borderLeft (px 3) solid "#22c55e"
+  ".status-change-text" ? do
+    fontWeight (weight 500)
+    color "#166534"
+  ".timeline-activity" ? do
+    display flex
+    alignItems center
+    Stylesheet.key "gap" ("6px" :: Text)
+    flexWrap Flexbox.wrap
+    padding (px 4) (px 0) (px 4) (px 0)
+    color "#6b7280"
+  ".activity-detail" ? do
+    fontSize (px 11)
+    color "#9ca3af"
+    fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
+  ".timeline-error" ? do
+    borderLeft (px 3) solid "#ef4444"
+    backgroundColor "#fef2f2"
+    padding (px 8) (px 12) (px 8) (px 12)
+    borderRadius (px 4) (px 4) (px 4) (px 4)
+  ".timeline-error" |> ".error-message" ? do
+    marginTop (px 6)
+    color "#dc2626"
+    fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
+    fontSize (px 12)
+    whiteSpace preWrap
+  ".timeline-thought" ? do
+    paddingLeft (px 4)
+  ".timeline-thought" |> ".thought-bubble" ? do
+    backgroundColor "#fef3c7"
+    padding (px 8) (px 12) (px 8) (px 12)
+    borderRadius (px 8) (px 8) (px 8) (px 8)
+    whiteSpace preWrap
+    marginTop (px 6)
+    fontSize (px 12)
+    lineHeight (em 1.5)
+  ".timeline-tool-call" ? do
+    borderLeft (px 3) solid "#3b82f6"
+    paddingLeft (px 8)
+  ".timeline-tool-call" |> "summary" ? do
+    cursor pointer
+    listStyleType none
+    display flex
+    alignItems center
+    Stylesheet.key "gap" ("6px" :: Text)
+  ".timeline-tool-call" |> "summary" # before ? do
+    content (stringContent "▶")
+    fontSize (px 10)
+    color "#6b7280"
+    transition "transform" (ms 150) ease (sec 0)
+  ".timeline-tool-call[open]" |> "summary" # before ? do
+    Stylesheet.key "transform" ("rotate(90deg)" :: Text)
+  ".timeline-tool-result" ? do
+    borderLeft (px 3) solid "#10b981"
+    paddingLeft (px 8)
+  ".timeline-tool-result" |> "summary" ? do
+    cursor pointer
+    listStyleType none
+    display flex
+    alignItems center
+    Stylesheet.key "gap" ("6px" :: Text)
+  ".timeline-cost" ? do
+    display flex
+    alignItems center
+    Stylesheet.key "gap" ("6px" :: Text)
+    fontSize (px 11)
+    color "#6b7280"
+    padding (px 2) (px 0) (px 2) (px 0)
+  ".timeline-checkpoint" ? do
+    borderLeft (px 3) solid "#8b5cf6"
+    backgroundColor "#faf5ff"
+    padding (px 8) (px 12) (px 8) (px 12)
+    borderRadius (px 4) (px 4) (px 4) (px 4)
+  ".timeline-checkpoint" |> ".checkpoint-content" ? do
+    marginTop (px 6)
+    fontSize (px 12)
+    whiteSpace preWrap
+  ".timeline-guardrail" ? do
+    borderLeft (px 3) solid "#f59e0b"
+    backgroundColor "#fffbeb"
+    padding (px 8) (px 12) (px 8) (px 12)
+    borderRadius (px 4) (px 4) (px 4) (px 4)
+  ".timeline-guardrail" |> ".guardrail-content" ? do
+    marginTop (px 6)
+    fontSize (px 12)
+    color "#92400e"
+  ".timeline-generic" ? do
+    padding (px 4) (px 0) (px 4) (px 0)
+    color "#6b7280"
+
 responsiveStyles :: Css
 responsiveStyles = do
   query Media.screen [Media.maxWidth (px 600)] <| do
@@ -1878,10 +2004,6 @@ darkModeStyles =
     ".retry-banner-details" ? color "#d1d5db"
     ".retry-value" ? color "#9ca3af"
     ".retry-commit" ? backgroundColor "#374151"
-    ".agent-log-section" ? borderTopColor "#374151"
-    ".agent-log-live" ? do
-      backgroundColor "#065f46"
-      color "#a7f3d0"
     ".event-bubble" ? backgroundColor "#374151"
     ".event-label" ? color "#d1d5db"
     ".line-count" ? do