feat(tui): integrate with Herdr host
This commit is contained in:
@@ -539,8 +539,8 @@ class WebSocketChannel(BaseChannel):
|
||||
self._webui_connections.discard(connection)
|
||||
self._discard_webui_request_lock_if_idle(connection)
|
||||
|
||||
async def _maybe_push_active_goal_state(self, chat_id: str) -> None:
|
||||
"""Replay an active sustained goal from session metadata after *chat_id* is subscribed.
|
||||
async def _maybe_push_persisted_goal_state(self, chat_id: str) -> None:
|
||||
"""Replay actionable goal state after *chat_id* is subscribed.
|
||||
|
||||
Goal metadata lives on the session JSONL and survives gateway restarts, but
|
||||
connected clients normally see it via ``goal_state`` / ``turn_end`` frames.
|
||||
@@ -554,7 +554,7 @@ class WebSocketChannel(BaseChannel):
|
||||
if not isinstance(meta, dict):
|
||||
meta = {}
|
||||
blob = goal_state_ws_blob(cast(dict[str, Any], meta))
|
||||
if not blob.get("active"):
|
||||
if not blob.get("active") and blob.get("status") != "blocked":
|
||||
return
|
||||
await self.send_goal_state(chat_id, blob)
|
||||
|
||||
@@ -572,7 +572,7 @@ class WebSocketChannel(BaseChannel):
|
||||
|
||||
async def _hydrate_after_subscribe(self, chat_id: str) -> None:
|
||||
"""Replay persisted or actively running per-chat state after subscribe."""
|
||||
await self._maybe_push_active_goal_state(chat_id)
|
||||
await self._maybe_push_persisted_goal_state(chat_id)
|
||||
await self._maybe_push_turn_run_wall_clock(chat_id)
|
||||
|
||||
async def _send_event(
|
||||
|
||||
@@ -3186,7 +3186,7 @@ async def test_maybe_push_active_goal_state_noop_without_session_manager() -> No
|
||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, gateway=_basic_handler(bus))
|
||||
mock_ws = AsyncMock()
|
||||
channel._attach(mock_ws, "chat-1")
|
||||
await channel._maybe_push_active_goal_state("chat-1")
|
||||
await channel._maybe_push_persisted_goal_state("chat-1")
|
||||
mock_ws.send.assert_not_called()
|
||||
|
||||
|
||||
@@ -3202,7 +3202,7 @@ async def test_maybe_push_active_goal_state_skips_when_no_goal_on_disk() -> None
|
||||
)
|
||||
mock_ws = AsyncMock()
|
||||
channel._attach(mock_ws, "chat-1")
|
||||
await channel._maybe_push_active_goal_state("chat-1")
|
||||
await channel._maybe_push_persisted_goal_state("chat-1")
|
||||
mock_ws.send.assert_not_called()
|
||||
|
||||
|
||||
@@ -3227,7 +3227,7 @@ async def test_maybe_push_active_goal_state_notifies_when_goal_active_on_disk()
|
||||
)
|
||||
mock_ws = AsyncMock()
|
||||
channel._attach(mock_ws, "chat-1")
|
||||
await channel._maybe_push_active_goal_state("chat-1")
|
||||
await channel._maybe_push_persisted_goal_state("chat-1")
|
||||
mock_ws.send.assert_awaited_once()
|
||||
body = json.loads(mock_ws.send.await_args.args[0])
|
||||
assert body["event"] == "goal_state"
|
||||
@@ -3237,6 +3237,39 @@ async def test_maybe_push_active_goal_state_notifies_when_goal_active_on_disk()
|
||||
assert body["goal_state"]["ui_summary"] == "Docs"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maybe_push_goal_state_restores_blocked_attention_on_disk() -> None:
|
||||
bus = MagicMock()
|
||||
sm = MagicMock()
|
||||
sm.read_session_file.return_value = {
|
||||
"metadata": {
|
||||
"goal_state": {
|
||||
"status": "blocked",
|
||||
"objective": "deploy safely",
|
||||
"ui_summary": "Approval required",
|
||||
},
|
||||
},
|
||||
"messages": [],
|
||||
}
|
||||
channel = WebSocketChannel(
|
||||
{"enabled": True, "allowFrom": ["*"]},
|
||||
bus,
|
||||
gateway=_basic_handler(bus, session_manager=sm),
|
||||
)
|
||||
mock_ws = AsyncMock()
|
||||
channel._attach(mock_ws, "chat-1")
|
||||
|
||||
await channel._maybe_push_persisted_goal_state("chat-1")
|
||||
|
||||
body = json.loads(mock_ws.send.await_args.args[0])
|
||||
assert body["goal_state"] == {
|
||||
"active": False,
|
||||
"status": "blocked",
|
||||
"ui_summary": "Approval required",
|
||||
"objective": "deploy safely",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maybe_push_turn_run_wall_clock_skips_when_no_active_turn() -> None:
|
||||
bus = MagicMock()
|
||||
|
||||
@@ -98,16 +98,20 @@ def goal_state_runtime_lines(metadata: Mapping[str, Any] | None) -> list[str]:
|
||||
def goal_state_ws_blob(metadata: Mapping[str, Any] | None) -> dict[str, Any]:
|
||||
"""JSON-safe snapshot for WebSocket ``goal_state`` events (one chat_id per frame)."""
|
||||
goal = parse_goal_state(_session_goal_raw(metadata)) if metadata else None
|
||||
if isinstance(goal, dict) and goal.get("status") == "active":
|
||||
if isinstance(goal, dict) and goal.get("status") in {"active", "blocked"}:
|
||||
status = str(goal.get("status"))
|
||||
objective = str(goal.get("objective") or "").strip()
|
||||
if len(objective) > _MAX_OBJECTIVE_WS:
|
||||
objective = objective[:_MAX_OBJECTIVE_WS].rstrip() + "…"
|
||||
summary = str(goal.get("ui_summary") or "").strip()[:120]
|
||||
blob: dict[str, Any] = {"active": True}
|
||||
blob: dict[str, Any] = {"active": status == "active", "status": status}
|
||||
if summary:
|
||||
blob["ui_summary"] = summary
|
||||
if objective:
|
||||
blob["objective"] = objective
|
||||
recap = str(goal.get("recap") or "").strip()[:240]
|
||||
if recap:
|
||||
blob["recap"] = recap
|
||||
return blob
|
||||
return {"active": False}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user