diff options
| author | Ben Sima <ben@bensima.com> | 2025-12-01 12:01:56 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-12-01 12:01:56 -0500 |
| commit | a16de8c9884f7eab639d1e5b016c9d6846866e03 (patch) | |
| tree | d1b6bc51df5e515a42f2a48f80be8475566f70ae /Omni/Jr | |
| parent | 046e6d1ca55651379f938b4481570bcb1b122e1e (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.hs | 378 | ||||
| -rw-r--r-- | Omni/Jr/Web/Style.hs | 182 |
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 |
