← Back to task

Commit cafebd24

commit cafebd24adab1d926e8531c22d7477d62d4596ff
Author: Ben Sima <ben@bensima.com>
Date:   Mon Dec 1 14:49:41 2025

    Clicking LIVE label toggles live updates on/off
    
    - Add clickable LIVE toggle button that pauses/resumes timeline polling
    - Green pulsing when active, grey when paused
    - Uses htmx:beforeRequest event to cancel requests when paused
    - Increase duplicate tool call guardrail from 20 to 30
    
    Task-Id: t-211

diff --git a/Omni/Agent/Worker.hs b/Omni/Agent/Worker.hs
index 0ad5a564..ea2593e7 100644
--- a/Omni/Agent/Worker.hs
+++ b/Omni/Agent/Worker.hs
@@ -317,7 +317,7 @@ runWithEngine worker repo task = do
             Engine.Guardrails
               { Engine.guardrailMaxCostCents = 200.0,
                 Engine.guardrailMaxTokens = 2000000,
-                Engine.guardrailMaxDuplicateToolCalls = 20,
+                Engine.guardrailMaxDuplicateToolCalls = 30,
                 Engine.guardrailMaxTestFailures = 3
               }
           agentCfg =
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index ba65a884..cd6f2d7b 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -414,6 +414,7 @@ pageHead title =
     Lucid.script_ [] statusDropdownJs
     Lucid.script_ [] priorityDropdownJs
     Lucid.script_ [] navbarDropdownJs
+    Lucid.script_ [] liveToggleJs
 
 navbarDropdownJs :: Text
 navbarDropdownJs =
@@ -2368,6 +2369,39 @@ commentForm tid =
         ""
       Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-primary"] "Post Comment"
 
+-- | Render the LIVE toggle button
+renderLiveToggle :: (Monad m) => Lucid.HtmlT m ()
+renderLiveToggle =
+  Lucid.button_
+    [ Lucid.class_ "timeline-live-toggle",
+      Lucid.id_ "live-toggle",
+      Lucid.makeAttribute "onclick" "toggleLiveUpdates()",
+      Lucid.title_ "Click to pause/resume live updates"
+    ]
+    " LIVE"
+
+-- | JavaScript for toggling live updates
+liveToggleJs :: Text
+liveToggleJs =
+  Text.unlines
+    [ "var liveUpdatesEnabled = true;",
+      "",
+      "function toggleLiveUpdates() {",
+      "  liveUpdatesEnabled = !liveUpdatesEnabled;",
+      "  var btn = document.getElementById('live-toggle');",
+      "  if (btn) {",
+      "    btn.classList.toggle('timeline-live-paused', !liveUpdatesEnabled);",
+      "  }",
+      "}",
+      "",
+      "document.body.addEventListener('htmx:beforeRequest', function(evt) {",
+      "  var timeline = document.getElementById('unified-timeline');",
+      "  if (timeline && timeline.contains(evt.target) && !liveUpdatesEnabled) {",
+      "    evt.preventDefault();",
+      "  }",
+      "});"
+    ]
+
 -- | 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
@@ -2385,7 +2419,7 @@ renderUnifiedTimeline tid legacyComments events status now = do
   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"
+      when isInProgress <| renderLiveToggle
 
     if null events && null legacyComments
       then Lucid.p_ [Lucid.class_ "empty-msg"] "No activity yet."
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
index bb74ce92..cf32570b 100644
--- a/Omni/Jr/Web/Style.hs
+++ b/Omni/Jr/Web/Style.hs
@@ -1577,6 +1577,26 @@ unifiedTimelineStyles = do
     marginTop (em 1.5)
     paddingTop (em 1)
     borderTop (px 1) solid "#e5e7eb"
+  ".timeline-live-toggle" ? 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
+    border (px 1) solid "#6ee7b7"
+    cursor pointer
+    Stylesheet.key "transition" ("all 0.3s ease" :: Text)
+    Stylesheet.key "animation" ("pulse 2s infinite" :: Text)
+  ".timeline-live-toggle:hover" ? do
+    Stylesheet.key "box-shadow" ("0 0 8px rgba(16,185,129,0.4)" :: Text)
+  ".timeline-live-toggle.timeline-live-paused" ? do
+    color "#6b7280"
+    backgroundColor "#f3f4f6"
+    border (px 1) solid "#d1d5db"
+    Stylesheet.key "animation" ("none" :: Text)
   ".timeline-live" ? do
     fontSize (px 10)
     fontWeight bold