Commit: 71a50df8
commit 71a50df8875dc88ba1dc0bba2bf606473db37cb5
Author: Coder Agent <coder@agents.omni>
Date: Mon Apr 20 17:41:15 2026
fix(agentd): IRC-style watch and stable typecheck deps
Finish persistent watch formatting as a chat-style stream with
user/assistant lines and /me status updates for thinking/tools.
Messages are no longer truncated in watch output, so terminal
wrapping handles long content naturally.
Also harden local rundep handling in repl/build plumbing so
typecheck.sh no longer tries to treat LocalRun paths as nixpkgs
packages.
Task-Id: t-819
Task-Id: t-818
diff --git a/Omni/Agentd.hs b/Omni/Agentd.hs
index 8fa0502e..66e5609f 100755
--- a/Omni/Agentd.hs
+++ b/Omni/Agentd.hs
@@ -3470,19 +3470,19 @@ test =
case formatPersistentWatchLine tz False "agent-x" line of
Nothing -> Test.assertFailure "Expected formatted line"
Just out ->
- Test.assertBool "formatted user line" ("15:01:15 [agent-x] user: hello world" `Text.isInfixOf` out),
- Test.unit "persistent watch formats tool calls" <| do
+ Test.assertBool "formatted user line" ("user> hello world" `Text.isInfixOf` out),
+ Test.unit "persistent watch formats tool calls as status lines" <| do
let tz = Time.minutesToTimeZone 0
line = "{\"type\":\"tool_call\",\"timestamp\":\"2026-04-11T15:01:15.946Z\",\"tool_name\":\"run_bash\",\"tool_args\":{\"command\":\"pwd && ls -la\"}}"
case formatPersistentWatchLine tz False "agent-x" line of
Nothing -> Test.assertFailure "Expected formatted line"
Just out ->
- Test.assertBool "formatted tool call line" ("tool_call run_bash cmd=pwd && ls -la" `Text.isInfixOf` out),
+ Test.assertBool "formatted tool call line" ("/me calls run_bash: pwd && ls -la" `Text.isInfixOf` out),
Test.unit "persistent watch normalizes legacy untyped result lines" <| do
let tz = Time.minutesToTimeZone 0
line = "{\"success\":true,\"response\":\"done\"}"
case formatPersistentWatchLine tz False "agent-x" line of
Nothing -> Test.assertFailure "Expected formatted line"
Just out ->
- Test.assertBool "formatted legacy result line" ("result ✓: done" `Text.isInfixOf` out)
+ Test.assertBool "formatted legacy result line" ("assistant> done" `Text.isInfixOf` out)
]
diff --git a/Omni/Agentd/PersistentView.hs b/Omni/Agentd/PersistentView.hs
index 06b3f243..b71dab8a 100644
--- a/Omni/Agentd/PersistentView.hs
+++ b/Omni/Agentd/PersistentView.hs
@@ -36,17 +36,11 @@ formatPersistentWatchLine timezone details runId line =
in if Text.null trimmed
then Nothing
else case decodeJsonObject line of
- Nothing -> Just <| watchPrefix timezone runId Nothing <> "raw " <> clipWatchText 120 trimmed
- Just obj ->
+ Nothing -> Just <| watchPrefix timezone runId Nothing <> "/me raw: " <> trimmed
+ Just obj -> do
+ body <- renderPersistentWatchEvent details obj
let timestamp = topLookupTextField "timestamp" obj
- in case topLookupTextField "type" obj of
- Nothing ->
- if details
- then Just <| watchPrefix timezone runId timestamp <> "event " <> clipWatchText 120 trimmed
- else Nothing
- Just _ ->
- let body = renderPersistentWatchEvent details obj
- in Just <| watchPrefix timezone runId timestamp <> body
+ pure <| watchPrefix timezone runId timestamp <> body
watchPrefix :: Time.TimeZone -> Text -> Maybe Text -> Text
watchPrefix timezone runId mTimestamp =
@@ -55,15 +49,15 @@ watchPrefix timezone runId mTimestamp =
formatWatchTimestamp :: Time.TimeZone -> Maybe Text -> Text
formatWatchTimestamp timezone mTimestamp =
case mTimestamp of
- Nothing -> "--:--:--"
+ Nothing -> "--:--"
Just ts ->
case parseWatchTimestamp ts of
- Nothing -> "--:--:--"
+ Nothing -> "--:--"
Just utc ->
Text.pack
<| Time.formatTime
Time.defaultTimeLocale
- "%H:%M:%S"
+ "%H:%M"
(Time.utcToLocalTime timezone utc)
parseWatchTimestamp :: Text -> Maybe Time.UTCTime
@@ -73,77 +67,85 @@ parseWatchTimestamp ts =
<|> Time.parseTimeM True Time.defaultTimeLocale "%Y-%m-%dT%H:%M:%S%Q%z" input
<|> Time.parseTimeM True Time.defaultTimeLocale "%Y-%m-%d %H:%M:%S%Q UTC" input
-renderPersistentWatchEvent :: Bool -> Aeson.Object -> Text
+renderPersistentWatchEvent :: Bool -> Aeson.Object -> Maybe Text
renderPersistentWatchEvent details obj =
case topLookupTextField "type" obj of
Just "session" ->
- "session cwd=" <> fromMaybe "?" (topLookupTextField "cwd" obj)
+ Just <| "/me session cwd=" <> fromMaybe "?" (topLookupTextField "cwd" obj)
Just "model_change" ->
let provider = fromMaybe "?" (topLookupTextField "provider" obj)
modelId = fromMaybe "?" (topLookupTextField "modelId" obj)
- in "model " <> provider <> "/" <> modelId
+ in Just <| "/me model " <> provider <> "/" <> modelId
Just "thinking_level_change" ->
- "thinking " <> fromMaybe "?" (topLookupTextField "thinkingLevel" obj)
+ Just <| "/me thinking level " <> fromMaybe "?" (topLookupTextField "thinkingLevel" obj)
Just "message" ->
- fromMaybe "message" (renderPersistentWatchMessage details obj)
+ renderPersistentWatchMessage details obj
Just "infer_start" ->
- let iter = maybe "" (\i -> " i=" <> tshow i) (topLookupIntField "iteration" obj)
- model = maybe "" (" model=" <>) (topLookupTextField "model" obj)
- in "infer_start" <> iter <> model
+ let suffix =
+ if details
+ then
+ Text.unwords
+ <| catMaybes
+ [ ("i=" <>) </ (tshow </ topLookupIntField "iteration" obj),
+ ("model=" <>) </ topLookupTextField "model" obj
+ ]
+ else ""
+ in Just <| "/me thinking…" <> (if Text.null suffix then "" else " " <> suffix)
Just "tool_call" ->
- let toolName = fromMaybe "tool" (topLookupTextField "tool_name" obj)
- argPreview =
+ let toolName = fromMaybe "tool" (topLookupTextField "tool_name" obj <|> topLookupTextField "name" obj)
+ argText =
case topLookupObjectField "tool_args" obj of
- Nothing -> ""
+ Nothing -> Nothing
Just argsObj ->
- if details
- then " " <> renderJsonObjectPreview 180 argsObj
- else case topLookupTextField "command" argsObj of
- Nothing -> ""
- Just cmd -> " cmd=" <> clipWatchText 90 cmd
- in "tool_call " <> toolName <> argPreview
+ topLookupTextField "command" argsObj
+ <|> if details then Just (renderJsonObject argsObj) else Nothing
+ in Just <| "/me calls " <> toolName <> maybe "" (": " <>) argText
Just "tool_result" ->
let toolName = fromMaybe "tool" (topLookupTextField "tool_name" obj)
ok = fromMaybe True (topLookupBoolField "success" obj)
icon = if ok then "✓" else "✗"
duration = maybe "" (\ms -> " (" <> tshow ms <> "ms)") (topLookupIntField "duration_ms" obj)
- outputPreview =
- case topLookupTextField "output" obj of
- Nothing -> ""
- Just out ->
- let limit = if details then 220 else 120
- in ": " <> clipWatchText limit out
- in "tool_result " <> icon <> " " <> toolName <> duration <> outputPreview
+ outputText = topLookupTextField "output" obj
+ base = "/me tool " <> toolName <> " " <> icon <> duration
+ in Just <| case (details, outputText) of
+ (True, Just out) -> base <> "\n" <> out
+ _ -> base
Just "infer_end" ->
- let iter = maybe "" (\i -> " i=" <> tshow i) (topLookupIntField "iteration" obj)
- tokens = maybe "" (\n -> " tok=" <> tshow n) (topLookupIntField "tokens" obj)
- duration = maybe "" (\ms -> " dur=" <> tshow ms <> "ms") (topLookupIntField "duration_ms" obj)
- cost = maybe "" (\c -> " cost=" <> formatCost c) (topLookupNumberField "cost_cents" obj)
- responsePreview =
+ let metricParts =
if details
- then case topLookupTextField "response_preview" obj of
- Nothing -> ""
- Just resp -> " | " <> clipWatchText 180 resp
- else ""
- in "infer_end" <> iter <> tokens <> duration <> cost <> responsePreview
+ then
+ catMaybes
+ [ ("tok=" <>) </ (tshow </ topLookupIntField "tokens" obj),
+ ("dur=" <>) </ ((<> "ms") <. tshow </ topLookupIntField "duration_ms" obj),
+ ("cost=" <>) </ (formatCost </ topLookupNumberField "cost_cents" obj)
+ ]
+ else []
+ preview = if details then topLookupTextField "response_preview" obj else Nothing
+ base =
+ "/me thinking done"
+ <> (if null metricParts then "" else " " <> Text.unwords metricParts)
+ in Just <| maybe base (\resp -> base <> "\n" <> resp) preview
Just "checkpoint" ->
- "checkpoint " <> fromMaybe "?" (topLookupTextField "name" obj)
+ Just <| "/me checkpoint " <> fromMaybe "?" (topLookupTextField "name" obj)
Just "result" ->
let ok = fromMaybe True (topLookupBoolField "success" obj)
- prefix = if ok then "result ✓" else "result ✗"
message =
topLookupTextField "response" obj
<|> topLookupTextField "error" obj
<|> topLookupTextField "message" obj
- in case message of
- Nothing -> prefix
- Just txt -> prefix <> ": " <> clipWatchText (if details then 220 else 120) txt
+ in if ok
+ then Just <| maybe "/me result ✓" ("assistant> " <>) message
+ else Just <| "/me result ✗" <> maybe "" (": " <>) message
Just "custom" ->
renderPersistentWatchCustom details obj
Just other ->
- "event " <> other
+ if details
+ then Just <| "/me event " <> other
+ else Nothing
Nothing ->
- "event"
+ if details
+ then Just "/me event"
+ else Nothing
renderPersistentWatchMessage :: Bool -> Aeson.Object -> Maybe Text
renderPersistentWatchMessage details obj = do
@@ -151,39 +153,31 @@ renderPersistentWatchMessage details obj = do
role <- topLookupTextField "role" msgObj
case role of
"user" ->
- let snippet = fromMaybe "(message)" (messageTextSnippet msgObj)
- limit = if details then 220 else 120
- in Just <| "user: " <> clipWatchText limit snippet
+ Just <| "user> " <> fromMaybe "(message)" (messageTextBody msgObj <|> messageThinkingBody msgObj)
"assistant" ->
- case messageToolCalls msgObj of
- tool : rest ->
- let preview =
- if null rest
- then tool
- else tool <> " +" <> tshow (length rest)
- in Just <| "assistant→tool " <> preview
- [] ->
- let snippet =
- fromMaybe "(assistant response)"
- <| messageTextSnippet msgObj
- <|> messageThinkingSnippet msgObj
- limit = if details then 220 else 120
- in Just <| "assistant: " <> clipWatchText limit snippet
+ case messageTextBody msgObj of
+ Just body -> Just <| "assistant> " <> body
+ Nothing ->
+ case messageToolCalls msgObj of
+ tool : rest ->
+ Just <| "/me calls " <> Text.intercalate ", " (tool : rest)
+ [] ->
+ case messageThinkingBody msgObj of
+ Just thought -> Just <| "/me thinking: " <> thought
+ Nothing -> Just "/me assistant update"
"toolResult" ->
let toolName = fromMaybe "tool" (topLookupTextField "toolName" msgObj)
isError = fromMaybe False (topLookupBoolField "isError" msgObj)
icon = if isError then "✗" else "✓"
- suffix =
- case messageTextSnippet msgObj of
- Nothing -> ""
- Just snippet ->
- let limit = if details then 220 else 120
- in ": " <> clipWatchText limit snippet
- in Just <| "tool_result " <> icon <> " " <> toolName <> suffix
+ base = "/me tool " <> toolName <> " " <> icon
+ body = messageTextBody msgObj <|> messageThinkingBody msgObj
+ in Just <| case (details, body) of
+ (True, Just txt) -> base <> "\n" <> txt
+ _ -> base
_ ->
- Just <| "message " <> role
+ Just <| "/me " <> role <> " message" <> maybe "" (": " <>) (messageTextBody msgObj <|> messageThinkingBody msgObj)
-renderPersistentWatchCustom :: Bool -> Aeson.Object -> Text
+renderPersistentWatchCustom :: Bool -> Aeson.Object -> Maybe Text
renderPersistentWatchCustom details obj =
let customType = fromMaybe "custom" (topLookupTextField "custom_type" obj)
message =
@@ -194,36 +188,56 @@ renderPersistentWatchCustom details obj =
<|> topLookupTextField "response" dataObj
<|> topLookupTextField "error" dataObj
<|> topLookupTextField "content" dataObj
- suffix =
- case message of
- Nothing -> ""
- Just msg ->
- let limit = if details then 220 else 120
- in ": " <> clipWatchText limit msg
- in customType <> suffix
+ in case customType of
+ "agent_complete" ->
+ Just <| maybe "/me agent complete" ("assistant> " <>) message
+ "agent_error" ->
+ Just <| "/me error" <> maybe "" (": " <>) message
+ "thinking" ->
+ Just <| "/me thinking" <> maybe "" (": " <>) message
+ "agent_start" ->
+ Just "/me agent started"
+ _ ->
+ case message of
+ Just msg -> Just <| "/me " <> customType <> ": " <> msg
+ Nothing -> if details then Just <| "/me " <> customType else Nothing
-messageThinkingSnippet :: Aeson.Object -> Maybe Text
-messageThinkingSnippet msgObj =
+messageTextBody :: Aeson.Object -> Maybe Text
+messageTextBody msgObj =
+ case KeyMap.lookup "content" msgObj of
+ Just (Aeson.Array parts) ->
+ let chunks =
+ [ txt
+ | Aeson.Object partObj <- Vector.toList parts,
+ topLookupTextField "type" partObj == Just "text",
+ Just txt <- [topLookupTextField "text" partObj <|> topLookupTextField "content" partObj]
+ ]
+ in joinMessageChunks chunks
+ Just (Aeson.String txt) -> Just txt
+ _ -> Nothing
+
+messageThinkingBody :: Aeson.Object -> Maybe Text
+messageThinkingBody msgObj =
case topLookupArrayField "content" msgObj of
Nothing -> Nothing
- Just parts -> listToMaybe (mapMaybe extractThinking parts)
- where
- extractThinking (Aeson.Object partObj) = do
- partType <- topLookupTextField "type" partObj
- guard (partType == "thinking")
- topLookupTextField "thinking" partObj
- extractThinking _ = Nothing
+ Just parts ->
+ let chunks =
+ [ txt
+ | Aeson.Object partObj <- parts,
+ topLookupTextField "type" partObj == Just "thinking",
+ Just txt <- [topLookupTextField "thinking" partObj <|> topLookupTextField "text" partObj <|> topLookupTextField "content" partObj]
+ ]
+ in joinMessageChunks chunks
-renderJsonObjectPreview :: Int -> Aeson.Object -> Text
-renderJsonObjectPreview limit obj =
- clipWatchText limit <| TE.decodeUtf8With TextEncodingError.lenientDecode (BL.toStrict (Aeson.encode (Aeson.Object obj)))
+joinMessageChunks :: [Text] -> Maybe Text
+joinMessageChunks chunks =
+ case filter (not <. Text.null <. Text.strip) chunks of
+ [] -> Nothing
+ keptChunks -> Just (Text.concat keptChunks)
-clipWatchText :: Int -> Text -> Text
-clipWatchText limit txt =
- let cleaned = Text.strip <| Text.replace "\n" " " txt
- in if Text.length cleaned > limit
- then Text.take limit cleaned <> "…"
- else cleaned
+renderJsonObject :: Aeson.Object -> Text
+renderJsonObject obj =
+ TE.decodeUtf8With TextEncodingError.lenientDecode (BL.toStrict (Aeson.encode (Aeson.Object obj)))
persistentLineLabel :: Text -> Maybe Text
persistentLineLabel line = do
diff --git a/Omni/Bild/Builder.nix b/Omni/Bild/Builder.nix
index 06a7d4aa..47096a40 100644
--- a/Omni/Bild/Builder.nix
+++ b/Omni/Bild/Builder.nix
@@ -151,8 +151,8 @@ with bild; let
else r.contents)
(builtins.filter (r:
if builtins.isString r
- then true
- else r.tag == "ExternalRun")
+ then r != "" && !(lib.strings.hasInfix "/" r)
+ else r.tag == "ExternalRun" && r.contents != "")
target.rundeps);
rundeps_ =
diff --git a/Omni/Ide/repl.sh b/Omni/Ide/repl.sh
index 62250787..1e1ae32a 100755
--- a/Omni/Ide/repl.sh
+++ b/Omni/Ide/repl.sh
@@ -31,9 +31,18 @@ fi
fi
targets="${*:?}"
json=$(bild --plan "${targets[@]}" 2>&1)
- mapfile -t langdeps < <(jq --raw-output '.[].langdeps | select(length > 0) | join("\n")' <<< "$json")
- mapfile -t sysdeps < <(jq --raw-output '.[].sysdeps | select(length > 0) | join("\n")' <<< "$json")
- mapfile -t rundeps < <(jq --raw-output '.[].rundeps | select(length > 0) | join("\n")' <<< "$json")
+ mapfile -t langdeps < <(jq --raw-output '.[] | .langdeps[]? | select(type == "string" and length > 0)' <<< "$json")
+ mapfile -t sysdeps < <(jq --raw-output '.[] | .sysdeps[]? | select(type == "string" and length > 0)' <<< "$json")
+ mapfile -t rundeps < <(
+ jq --raw-output '
+ .[]
+ | .rundeps[]?
+ | if type == "string"
+ then select((contains("/") | not) and length > 0)
+ else select(.tag == "ExternalRun") | .contents | select(type == "string" and length > 0)
+ end
+ ' <<< "$json"
+ )
exts=$(jq --raw-output '.[].namespace.ext' <<< "$json" | sort | uniq)
packageSet=$(jq --raw-output '.[].packageSet' <<< "$json")
module=$(jq --raw-output '.[].mainModule' <<< "$json")