diff options
| author | Omni Worker <bot@omni.agent> | 2025-11-21 22:14:32 -0500 |
|---|---|---|
| committer | Omni Worker <bot@omni.agent> | 2025-11-21 22:14:32 -0500 |
| commit | c93d42b9e5bda51f463c13d4d227fc8cadf56628 (patch) | |
| tree | 1a696a85accc8d1c8d5953b2a4ad3bf4e4e15f8a /Omni/Agent/Log.hs | |
| parent | d181f08648cc903867f0c10e5632a973e92ae36a (diff) | |
feat(agent): add 2-line status monitoring UI
Amp-Thread-ID:
https://ampcode.com/threads/T-79499d9e-f4f4-40de-893c-524c32a45483
Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'Omni/Agent/Log.hs')
| -rw-r--r-- | Omni/Agent/Log.hs | 196 |
1 files changed, 88 insertions, 108 deletions
diff --git a/Omni/Agent/Log.hs b/Omni/Agent/Log.hs index 28dbab2..6541551 100644 --- a/Omni/Agent/Log.hs +++ b/Omni/Agent/Log.hs @@ -1,123 +1,103 @@ -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoImplicitPrelude #-} -module Omni.Agent.Log - ( LogEntry (..), - Status (..), - initialStatus, - updateStatus, - renderStatus, - parseLine, - format, - ) -where +-- : out omni-agent-log +module Omni.Agent.Log where import Alpha -import Data.Aeson (FromJSON (..), (.:), (.:?)) -import qualified Data.Aeson as Aeson -import qualified Data.ByteString.Lazy as BSL -import qualified Data.Set as Set - -data LogEntry = LogEntry - { leMessage :: Text, - leLevel :: Maybe Text, - leToolName :: Maybe Text, - leBatches :: Maybe [[Text]], - leMethod :: Maybe Text, - lePath :: Maybe Text, - leTimestamp :: Maybe Text - } - deriving (Show, Eq, Generic) - -instance FromJSON LogEntry where - parseJSON = - Aeson.withObject "LogEntry" <| \v -> - ( LogEntry - </ (v .: "message") - ) - <*> v - .:? "level" - <*> v - .:? "toolName" - <*> v - .:? "batches" - <*> v - .:? "method" - <*> v - .:? "path" - <*> v - .:? "timestamp" +import qualified Data.Text.IO as TIO +import qualified System.Console.ANSI as ANSI +import qualified System.IO as IO +import System.IO.Unsafe (unsafePerformIO) +import Data.IORef (IORef, newIORef, readIORef, writeIORef, modifyIORef') +-- | Status of the agent for the UI data Status = Status - { sWorkerName :: Text, - sTaskId :: Maybe Text, - sFiles :: Set.Set Text, - sStartTime :: Maybe Text, - sLastActivity :: Text + { statusWorker :: Text, + statusTask :: Maybe Text, + statusFiles :: Int, + statusCredits :: Double, + statusTime :: Text, -- formatted time string + statusActivity :: Text } - deriving (Show, Eq, Generic) + deriving (Show, Eq) -initialStatus :: Text -> Status -initialStatus name = +emptyStatus :: Text -> Status +emptyStatus workerName = Status - { sWorkerName = name, - sTaskId = Nothing, - sFiles = Set.empty, - sStartTime = Nothing, - sLastActivity = "Idle" + { statusWorker = workerName, + statusTask = Nothing, + statusFiles = 0, + statusCredits = 0.0, + statusTime = "00:00", + statusActivity = "Idle" } -updateStatus :: LogEntry -> Status -> Status -updateStatus e s = - let s' = case format e of - Just msg -> s {sLastActivity = msg} - Nothing -> s - s'' = case leTimestamp e of - Just t -> if isNothing (sStartTime s) then s' {sStartTime = Just t} else s' - Nothing -> s' - in case (leMessage e, lePath e) of - ("ide-fs", Just p) -> s'' {sFiles = Set.insert p (sFiles s'')} - _ -> s'' +-- | Global state for the status bar +{-# NOINLINE currentStatus #-} +currentStatus :: IORef Status +currentStatus = unsafePerformIO (newIORef (emptyStatus "Unknown")) + +-- | Initialize the status bar system +init :: Text -> IO () +init workerName = do + writeIORef currentStatus (emptyStatus workerName) + -- Reserve 2 lines at bottom + IO.hPutStrLn IO.stderr "" + IO.hPutStrLn IO.stderr "" + ANSI.hCursorUp IO.stderr 2 + +-- | Update the status +update :: (Status -> Status) -> IO () +update f = do + modifyIORef' currentStatus f + render -renderStatus :: Status -> Text -renderStatus s = - let line1 = - "[Worker: " - <> sWorkerName s - <> "] " - <> "Task: " - <> fromMaybe "None" (sTaskId s) - <> " | Files: " - <> show (Set.size (sFiles s)) - line2 = sLastActivity s - in line1 <> "\n" <> line2 +-- | Set the activity message +updateActivity :: Text -> IO () +updateActivity msg = update (\s -> s {statusActivity = msg}) -parseLine :: Text -> Maybe LogEntry -parseLine line = Aeson.decode <| BSL.fromStrict <| encodeUtf8 line +-- | Log a scrolling message (appears above status bars) +log :: Text -> IO () +log msg = do + -- Clear status bars + ANSI.hClearLine IO.stderr + ANSI.hCursorDown IO.stderr 1 + ANSI.hClearLine IO.stderr + ANSI.hCursorUp IO.stderr 1 + + -- Print message (scrolls screen) + TIO.hPutStrLn IO.stderr msg + + -- Re-render status bars at bottom + -- (Since we scrolled, we are now on the line above where the first status line should be) + render -format :: LogEntry -> Maybe Text -format e = - case leMessage e of - "executing 1 tools in 1 batch(es)" -> - let tool = case leBatches e of - Just ((t : _) : _) -> t - _ -> "unknown" - in Just <| "🤖 THOUGHT: Planning tool execution (" <> tool <> ")" - "Tool Bash permitted - action: allow" -> - Just "🔧 TOOL: Bash command executed" - msg - | "Processing tool completion for ledger" == msg && isJust (leToolName e) -> - Just <| "✅ TOOL: " <> fromMaybe "" (leToolName e) <> " completed" - "ide-fs" -> - case leMethod e of - Just "readFile" -> Just <| "📂 READ: " <> fromMaybe "" (lePath e) - _ -> Nothing - "System prompt build complete (no changes)" -> - Just "🧠 THINKING..." - "System prompt build complete (first build)" -> - Just "🚀 STARTING new task context" - msg -> - case leLevel e of - Just "error" -> Just <| "❌ ERROR: " <> msg - _ -> Nothing +-- | Render the two status lines +render :: IO () +render = do + Status {..} <- readIORef currentStatus + + -- Line 1: Meta + -- [Worker: name] Task: t-123 | Files: 3 | Credits: $0.45 | Time: 05:23 + let taskStr = maybe "None" identity statusTask + meta = "[Worker: " <> statusWorker <> "] Task: " <> taskStr + <> " | Files: " <> tshow statusFiles + <> " | Credits: $" <> tshow statusCredits + <> " | Time: " <> statusTime + + ANSI.hSetCursorColumn IO.stderr 0 + ANSI.hClearLine IO.stderr + TIO.hPutStr IO.stderr meta + + -- Line 2: Activity + -- [14:05:22] 🤖 Thinking... + ANSI.hCursorDown IO.stderr 1 + ANSI.hSetCursorColumn IO.stderr 0 + ANSI.hClearLine IO.stderr + TIO.hPutStr IO.stderr ("🤖 " <> statusActivity) + + -- Return cursor to line 1 + ANSI.hCursorUp IO.stderr 1 + IO.hFlush IO.stderr |
