← Back to task

Commit d701ca93

commit d701ca93850a2572eeea4029a3d53cd3138e601b
Author: Ben Sima <ben@bensima.com>
Date:   Mon Dec 1 07:50:29 2025

    Fix agent event content double-encoding in web UI
    
    Excellent! The changes have been successfully applied. Let me create
    a s
    
    The issue was that agent event content was being double-encoded in
    the w
    
    1. **ToolResult events** showed raw JSON like `{"output":"Replaced 1
    occ 2. **Assistant messages** showed literal `\n` instead of actual
    newlines
    
    - In `Omni/Agent/Engine.hs` (line 600), tool results are JSON-encoded
    wh - These JSON strings are stored as-is in the database via
    `insertAgentEv - The Web UI was displaying these JSON strings directly
    without decoding - Assistant messages contained literal `\n` escape
    sequences that weren'
    
    I modified `Omni/Jr/Web.hs` with the following changes:
    
    1. **Added import**: `Data.Aeson.KeyMap` to work with JSON objects
    
    2. **Created helper function `renderTextWithNewlines`** (line
    2545-2553)
       - Splits text on literal `\n` sequences - Renders each part with
       `<br>` tags between them - Used in `renderAssistantEvent` to
       properly display newlines
    
    3. **Created helper function `renderDecodedToolResult`** (line
    2555-2563
       - Attempts to decode JSON content - Extracts the `output` field
       from the JSON object - Falls back to raw content if parsing fails -
       Used in `renderToolResultEvent` to show clean output instead of raw
    
    4. **Updated `renderAssistantEvent`** (line 2473):
       - Changed from `Lucid.toHtml truncated` to `renderTextWithNewlines
       tr
    
    5. **Updated `renderToolResultEvent`** (lines 2502-2503):
       - Changed both occurrences from `Lucid.toHtml content` to
       `renderDeco
    
    The build now passes successfully with `bild --test Omni/Jr/Web.hs`.
    
    Task-Id: t-200

diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index d1914544..aa8d4dee 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -19,6 +19,7 @@ where
 import Alpha
 import qualified Control.Concurrent as Concurrent
 import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.KeyMap as KeyMap
 import qualified Data.ByteString.Lazy as LBS
 import qualified Data.List as List
 import qualified Data.Text as Text
@@ -2469,7 +2470,7 @@ renderAssistantEvent content timestamp now =
     Lucid.div_ [Lucid.class_ "event-content event-bubble"] <| do
       let truncated = Text.take 2000 content
           isTruncated = Text.length content > 2000
-      Lucid.toHtml truncated
+      renderTextWithNewlines truncated
       when isTruncated <| Lucid.span_ [Lucid.class_ "event-truncated"] "..."
 
 renderToolCallEvent :: (Monad m) => Text -> UTCTime -> UTCTime -> Lucid.HtmlT m ()
@@ -2498,8 +2499,8 @@ renderToolResultEvent content timestamp now =
           then
             Lucid.details_ [Lucid.class_ "result-collapsible"] <| do
               Lucid.summary_ "Show output"
-              Lucid.pre_ [Lucid.class_ "event-content tool-output"] (Lucid.toHtml content)
-          else Lucid.pre_ [Lucid.class_ "event-content tool-output"] (Lucid.toHtml content)
+              Lucid.pre_ [Lucid.class_ "event-content tool-output"] (renderDecodedToolResult content)
+          else Lucid.pre_ [Lucid.class_ "event-content tool-output"] (renderDecodedToolResult content)
 
 renderCostEvent :: (Monad m) => Text -> Lucid.HtmlT m ()
 renderCostEvent content =
@@ -2540,6 +2541,26 @@ renderCollapsibleOutput content =
             Lucid.pre_ [Lucid.class_ "tool-output-pre"] (Lucid.toHtml content)
         else Lucid.pre_ [Lucid.class_ "tool-output-pre"] (Lucid.toHtml content)
 
+-- | Render text with literal \n replaced by <br> tags
+renderTextWithNewlines :: (Monad m) => Text -> Lucid.HtmlT m ()
+renderTextWithNewlines txt =
+  let parts = Text.splitOn "\\n" txt
+   in traverse_ renderPart (zip [0 ..] parts)
+  where
+    renderPart (idx, part) = do
+      Lucid.toHtml part
+      when (idx < length parts - 1) <| Lucid.br_ []
+
+-- | Decode JSON tool result and render in a user-friendly way
+renderDecodedToolResult :: (Monad m) => Text -> Lucid.HtmlT m ()
+renderDecodedToolResult content =
+  case Aeson.decode (LBS.fromStrict (str content)) of
+    Just (Aeson.Object obj) ->
+      case KeyMap.lookup "output" obj of
+        Just (Aeson.String output) -> Lucid.toHtml output
+        _ -> 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 =
   Lucid.script_