feat(tui): integrate with Herdr host

This commit is contained in:
Xubin Ren
2026-08-17 20:56:10 +08:00
parent 783d381710
commit 79d51be71f
17 changed files with 622 additions and 32 deletions
+4 -4
View File
@@ -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()