summaryrefslogtreecommitdiff
path: root/Omni/Jr
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-12-01 12:01:56 -0500
committerBen Sima <ben@bensima.com>2025-12-01 12:01:56 -0500
commita16de8c9884f7eab639d1e5b016c9d6846866e03 (patch)
treed1b6bc51df5e515a42f2a48f80be8475566f70ae /Omni/Jr
parent046e6d1ca55651379f938b4481570bcb1b122e1e (diff)
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
Diffstat (limited to 'Omni/Jr')
-rw-r--r--Omni/Jr/Web.hs378
-rw-r--r--Omni/Jr/Web/Style.hs182
2 files changed, 367 insertions, 193 deletions
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index f7a2219..2fc2064 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 1f11255..a169cd7 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