diff --git a/src/dashboard/routes/world.py b/src/dashboard/routes/world.py index 51daea0..be4d4c4 100644 --- a/src/dashboard/routes/world.py +++ b/src/dashboard/routes/world.py @@ -69,17 +69,11 @@ def _build_world_state(presence: dict) -> dict: } -@router.get("/state") -async def get_world_state() -> JSONResponse: - """Return Timmy's current world state for Workshop bootstrap. - - Reads from ``~/.timmy/presence.json`` if fresh, otherwise - rebuilds live from cognitive state. - """ +def _get_current_state() -> dict: + """Build the current world-state dict from best available source.""" presence = _read_presence_file() if presence is None: - # Fallback: build live from cognitive tracker try: from timmy.workshop_state import get_state_dict @@ -96,8 +90,18 @@ async def get_world_state() -> JSONResponse: "concerns": [], } + return _build_world_state(presence) + + +@router.get("/state") +async def get_world_state() -> JSONResponse: + """Return Timmy's current world state for Workshop bootstrap. + + Reads from ``~/.timmy/presence.json`` if fresh, otherwise + rebuilds live from cognitive state. + """ return JSONResponse( - content=_build_world_state(presence), + content=_get_current_state(), headers={"Cache-Control": "no-cache, no-store"}, ) @@ -109,10 +113,21 @@ async def get_world_state() -> JSONResponse: @router.websocket("/ws") async def world_ws(websocket: WebSocket) -> None: - """Accept a Workshop client and keep it alive for state broadcasts.""" + """Accept a Workshop client and keep it alive for state broadcasts. + + Sends a full ``world_state`` snapshot immediately on connect so the + client never starts from a blank slate. + """ await websocket.accept() _ws_clients.append(websocket) logger.info("World WS connected — %d clients", len(_ws_clients)) + + # Send full world-state snapshot so client bootstraps instantly + try: + snapshot = _get_current_state() + await websocket.send_text(json.dumps({"type": "world_state", **snapshot})) + except Exception as exc: + logger.warning("Failed to send WS snapshot: %s", exc) try: while True: await websocket.receive_text() # keep-alive diff --git a/tests/dashboard/test_world_api.py b/tests/dashboard/test_world_api.py index b7224c3..ddbbb80 100644 --- a/tests/dashboard/test_world_api.py +++ b/tests/dashboard/test_world_api.py @@ -219,3 +219,29 @@ def test_world_ws_endpoint_accepts_connection(client): """WebSocket endpoint at /api/world/ws accepts connections.""" with client.websocket_connect("/api/world/ws"): pass # Connection accepted — just close it + + +def test_world_ws_sends_snapshot_on_connect(client, tmp_path): + """WebSocket sends a world_state snapshot immediately on connect.""" + f = tmp_path / "presence.json" + f.write_text( + json.dumps( + { + "version": 1, + "liveness": "2026-03-19T02:00:00Z", + "mood": "exploring", + "current_focus": "testing", + "active_threads": [], + "recent_events": [], + "concerns": [], + } + ) + ) + with patch("dashboard.routes.world.PRESENCE_FILE", f): + with client.websocket_connect("/api/world/ws") as ws: + msg = json.loads(ws.receive_text()) + + assert msg["type"] == "world_state" + assert msg["timmyState"]["mood"] == "exploring" + assert msg["timmyState"]["activity"] == "testing" + assert "updatedAt" in msg