summaryrefslogtreecommitdiff
path: root/Omni/Jr
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-12-01 14:59:52 -0500
committerBen Sima <ben@bensima.com>2025-12-01 14:59:52 -0500
commit5b3256b500ebb9a2f953c8b54917626b11988448 (patch)
tree8362b05a1629b2311b25a218b867abec72378d70 /Omni/Jr
parentcafebd24adab1d926e8531c22d7477d62d4596ff (diff)
Consolidate cost display in timeline header, remove Execution Details
- Add cost/token summary to timeline header - Filter Cost events from timeline display - Remove Execution Details section (cost info now in header) - Remove unused renderExecutionDetails function Task-Id: t-216
Diffstat (limited to 'Omni/Jr')
-rw-r--r--Omni/Jr/Web.hs164
1 files changed, 45 insertions, 119 deletions
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index cd6f2d7..a493395 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -1518,7 +1518,7 @@ instance Lucid.ToHtml TaskDetailPage where
"The task "
Lucid.code_ (Lucid.toHtml tid)
" could not be found."
- toHtml (TaskDetailFound task allTasks activities maybeRetry commits maybeAggMetrics agentEvents now) =
+ toHtml (TaskDetailFound task allTasks _activities maybeRetry commits maybeAggMetrics agentEvents now) =
let crumbs = taskBreadcrumbs allTasks task
in Lucid.doctypehtml_ <| do
pageHead (TaskCore.taskId task <> " - Jr")
@@ -1588,21 +1588,6 @@ instance Lucid.ToHtml TaskDetailPage where
Lucid.div_ [Lucid.class_ "commit-list"] <| do
traverse_ (renderCommit (TaskCore.taskId task)) commits
- let hasRunningActivity = any (\a -> TaskCore.activityStage a == TaskCore.Running) activities
- when hasRunningActivity <| do
- let isInProgress = TaskCore.taskStatus task == TaskCore.InProgress
- htmxAttrs =
- [ Lucid.makeAttribute "hx-get" ("/partials/task/" <> TaskCore.taskId task <> "/metrics"),
- Lucid.makeAttribute "hx-trigger" "every 5s",
- Lucid.makeAttribute "hx-swap" "innerHTML"
- ]
- innerAttrs =
- [Lucid.id_ "execution-details-inner"]
- <> [attr | isInProgress, attr <- htmxAttrs]
- Lucid.div_ [Lucid.class_ "execution-section"] <| do
- Lucid.h3_ "Execution Details"
- Lucid.div_ innerAttrs <| renderExecutionDetails (TaskCore.taskId task) activities maybeRetry
-
when (TaskCore.taskStatus task == TaskCore.Review) <| do
Lucid.div_ [Lucid.class_ "review-link-section"] <| do
Lucid.a_
@@ -1641,98 +1626,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"))
- 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
- in if null runningActs
- then Lucid.p_ [Lucid.class_ "empty-msg"] "No worker execution data available."
- else
- Lucid.div_ [Lucid.class_ "execution-details"] <| do
- let totalCost = sum [c | act <- runningActs, Just c <- [TaskCore.activityCostCents act]]
- totalDuration = sum [calcDurSecs act | act <- runningActs]
- attemptCount = length runningActs
-
- case retryCtx of
- Nothing -> pure ()
- Just ctx ->
- Lucid.div_ [Lucid.class_ "metric-row"] <| do
- Lucid.span_ [Lucid.class_ "metric-label"] "Retry Attempt:"
- Lucid.span_ [Lucid.class_ "metric-value retry-count"] (Lucid.toHtml (tshow (TaskCore.retryAttempt ctx) <> "/3"))
-
- when (attemptCount > 1) <| do
- Lucid.div_ [Lucid.class_ "metric-row"] <| do
- Lucid.span_ [Lucid.class_ "metric-label"] "Total Attempts:"
- Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (tshow attemptCount))
- Lucid.div_ [Lucid.class_ "metric-row"] <| do
- Lucid.span_ [Lucid.class_ "metric-label"] "Total Duration:"
- Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatDurSecs totalDuration))
- when (totalCost > 0)
- <| Lucid.div_ [Lucid.class_ "metric-row"]
- <| do
- Lucid.span_ [Lucid.class_ "metric-label"] "Total Cost:"
- Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatCostVal totalCost))
- Lucid.hr_ [Lucid.class_ "attempts-divider"]
-
- traverse_ (renderAttempt attemptCount) (zip [1 ..] (reverse runningActs))
- where
- calcDurSecs :: TaskCore.TaskActivity -> Int
- calcDurSecs act = case (TaskCore.activityStartedAt act, TaskCore.activityCompletedAt act) of
- (Just start, Just end) -> floor (diffUTCTime end start)
- _ -> 0
-
- formatDurSecs :: Int -> Text
- formatDurSecs secs
- | secs < 60 = tshow secs <> "s"
- | secs < 3600 = tshow (secs `div` 60) <> "m " <> tshow (secs `mod` 60) <> "s"
- | otherwise = tshow (secs `div` 3600) <> "h " <> tshow ((secs `mod` 3600) `div` 60) <> "m"
-
- renderAttempt :: (Monad m) => Int -> (Int, TaskCore.TaskActivity) -> Lucid.HtmlT m ()
- renderAttempt totalAttempts (attemptNum, act) = do
- when (totalAttempts > 1)
- <| Lucid.div_ [Lucid.class_ "attempt-header"] (Lucid.toHtml ("Attempt " <> tshow attemptNum :: Text))
- case TaskCore.activityThreadUrl act of
- Nothing -> pure ()
- Just url ->
- Lucid.div_ [Lucid.class_ "metric-row"] <| do
- Lucid.span_ [Lucid.class_ "metric-label"] "Session:"
- Lucid.a_ [Lucid.href_ url, Lucid.target_ "_blank", Lucid.class_ "amp-thread-btn"] "View in Amp ↗"
-
- case (TaskCore.activityStartedAt act, TaskCore.activityCompletedAt act) of
- (Just start, Just end) ->
- Lucid.div_ [Lucid.class_ "metric-row"] <| do
- Lucid.span_ [Lucid.class_ "metric-label"] "Duration:"
- Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatDur start end))
- (Just start, Nothing) ->
- Lucid.div_ [Lucid.class_ "metric-row"] <| do
- Lucid.span_ [Lucid.class_ "metric-label"] "Started:"
- Lucid.span_ [Lucid.class_ "metric-value"] (renderRelativeTimestamp now start)
- _ -> pure ()
-
- case TaskCore.activityCostCents act of
- Nothing -> pure ()
- Just cents ->
- Lucid.div_ [Lucid.class_ "metric-row"] <| do
- Lucid.span_ [Lucid.class_ "metric-label"] "Cost:"
- Lucid.span_ [Lucid.class_ "metric-value"] (Lucid.toHtml (formatCostVal cents))
-
- Lucid.div_ [Lucid.class_ "metric-row"] <| do
- Lucid.span_ [Lucid.class_ "metric-label"] "Timestamp:"
- Lucid.span_ [Lucid.class_ "metric-value"] (renderRelativeTimestamp now (TaskCore.activityTimestamp act))
-
- formatDur :: UTCTime -> UTCTime -> Text
- formatDur start end =
- let diffSecs = floor (diffUTCTime end start) :: Int
- mins = diffSecs `div` 60
- secs = diffSecs `mod` 60
- in if mins > 0
- then tshow mins <> "m " <> tshow secs <> "s"
- else tshow secs <> "s"
-
- formatCostVal :: Int -> Text
- formatCostVal cents =
- let dollars = fromIntegral cents / 100.0 :: Double
- in "$" <> Text.pack (showFFloat (Just 2) dollars "")
-
renderAggregatedMetrics :: (Monad m) => [TaskCore.Task] -> TaskCore.Task -> TaskCore.AggregatedMetrics -> Lucid.HtmlT m ()
renderAggregatedMetrics allTasks task metrics =
let descendants = getDescendants allTasks (TaskCore.taskId task)
@@ -2402,6 +2295,36 @@ liveToggleJs =
"});"
]
+-- | Aggregate cost and token data from events (Cost event type)
+aggregateCostMetrics :: [TaskCore.StoredEvent] -> (Int, Int)
+aggregateCostMetrics events =
+ let costEvents = filter (\e -> TaskCore.storedEventType e == "Cost") events
+ aggregateOne (totalCents, totalTokens) event =
+ case Aeson.decode (LBS.fromStrict (str (TaskCore.storedEventContent event))) of
+ Just (Aeson.Object obj) ->
+ let cents = case KeyMap.lookup "cents" obj of
+ Just (Aeson.Number n) -> floor n
+ _ -> 0
+ tokens = case KeyMap.lookup "tokens" obj of
+ Just (Aeson.Number n) -> floor n
+ _ -> 0
+ in (totalCents + cents, totalTokens + tokens)
+ _ -> (totalCents, totalTokens)
+ in foldl' aggregateOne (0, 0) costEvents
+
+-- | Format cost in dollars
+formatCostHeader :: Int -> Text
+formatCostHeader cents =
+ let dollars = fromIntegral cents / 100.0 :: Double
+ in "$" <> Text.pack (showFFloat (Just 2) dollars "")
+
+-- | Format tokens with K/M suffixes
+formatTokensHeader :: Int -> Text
+formatTokensHeader t
+ | t < 1000 = tshow t
+ | t < 1000000 = Text.pack (showFFloat (Just 1) (fromIntegral t / 1000.0 :: Double) "") <> "K"
+ | otherwise = Text.pack (showFFloat (Just 2) (fromIntegral t / 1000000.0 :: Double) "") <> "M"
+
-- | 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
@@ -2416,16 +2339,26 @@ renderUnifiedTimeline tid legacyComments events status now = do
Lucid.makeAttribute "hx-on::after-swap" "var log = this.querySelector('.timeline-events'); if(log && this.dataset.scroll) log.scrollTop = this.dataset.scroll"
]
else []
+ -- Count non-Cost events for the display
+ nonCostEvents = filter (\e -> TaskCore.storedEventType e /= "Cost") events
+ eventCount = length nonCostEvents + length legacyComments
+ (totalCents, totalTokens) = aggregateCostMetrics events
Lucid.div_ ([Lucid.class_ "unified-timeline-section", Lucid.id_ "unified-timeline"] <> pollAttrs) <| do
Lucid.h3_ <| do
- Lucid.toHtml ("Timeline (" <> tshow (length events + length legacyComments) <> ")")
+ Lucid.toHtml ("Timeline (" <> tshow eventCount <> ")")
+ when (totalCents > 0 || totalTokens > 0) <| do
+ Lucid.span_ [Lucid.class_ "timeline-cost-summary"] <| do
+ metaSep
+ when (totalCents > 0) <| Lucid.toHtml (formatCostHeader totalCents)
+ when (totalCents > 0 && totalTokens > 0) <| metaSep
+ when (totalTokens > 0) <| Lucid.toHtml (formatTokensHeader totalTokens <> " tokens")
when isInProgress <| renderLiveToggle
- if null events && null legacyComments
+ if null nonCostEvents && 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
+ traverse_ (renderTimelineEvent now) nonCostEvents
when isInProgress <| timelineScrollScript
commentForm tid
@@ -2456,7 +2389,7 @@ renderTimelineEvent now event =
"Assistant" -> renderAssistantTimelineEvent content actor timestamp now
"ToolCall" -> renderToolCallTimelineEvent content actor timestamp now
"ToolResult" -> renderToolResultTimelineEvent content actor timestamp now
- "Cost" -> renderCostTimelineEvent content
+ "Cost" -> pure () -- Cost events are hidden; cost data shown in timeline header
"Checkpoint" -> renderCheckpointEvent content actor timestamp now
"Guardrail" -> renderGuardrailEvent content actor timestamp now
_ -> renderGenericEvent eventType content actor timestamp now
@@ -2585,13 +2518,6 @@ renderToolResultTimelineEvent content _actor timestamp now =
renderRelativeTimestamp now timestamp
Lucid.pre_ [Lucid.class_ "event-content tool-output"] (renderDecodedToolResult content)
--- | 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)
-
-- | Render checkpoint event
renderCheckpointEvent :: (Monad m) => Text -> TaskCore.CommentAuthor -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
renderCheckpointEvent content actor timestamp now =