"""REST API helpers for the terminal client.
All helpers are ``async`` and share one ``httpx.AsyncClient`` (connection
pooling) so callers running inside the Textual event loop never block the UI on
a network round-trip — the await yields the loop while the request is in flight.
The shared client is created lazily on first use (inside a running loop) and
lives for the process; tests monkeypatch these functions wholesale, so the
client is never constructed in unit tests.
"""
from __future__ import annotations
import httpx
from clients.terminal.config import settings
# Lazily created on first use (in an async context). Reused across calls so
# HTTP connections are pooled rather than re-established per request.
_async_client: httpx.AsyncClient | None = None
def _client() -> httpx.AsyncClient:
global _async_client
if _async_client is None:
_async_client = httpx.AsyncClient(base_url=settings.base_url, timeout=30.0)
return _async_client
async def get_profiles() -> list[dict]:
client = _client()
resp = await client.get("/agents/profiles")
resp.raise_for_status()
return resp.json()
async def get_profile(profile_id: str) -> dict | None:
"""Return the profile dict for ``profile_id`` from /agents/profiles, or None."""
for p in await get_profiles():
if p.get("id") == profile_id:
return p
return None
async def get_profile_model(profile_id: str) -> str | None:
"""Configured model for a profile (first item of its priority list), or None.
Used to show a model in the status panel before the first request resolves
the actually-served model. ``profile.model`` is a priority list; the first
entry is the intended model.
"""
profile = await get_profile(profile_id)
if not profile:
return None
model = profile.get("model")
if isinstance(model, list):
return model[0] if model else None
return model or None
async def list_sessions() -> list[dict]:
client = _client()
resp = await client.get("/sessions")
resp.raise_for_status()
return resp.json()
async def create_session(profile_id: str | None = None) -> dict:
body: dict = {}
if profile_id:
body["profile_id"] = profile_id
client = _client()
resp = await client.post("/sessions", json=body)
resp.raise_for_status()
return resp.json()
async def get_session(session_id: str) -> dict:
client = _client()
resp = await client.get(f"/sessions/{session_id}")
resp.raise_for_status()
return resp.json()
async def get_todos(session_id: str) -> dict:
"""Fetch the session's current todo list (GET /sessions/{id}/todos).
Returns ``{"session_id": ..., "tasks": [{"index", "text", "status", "validation"}, ...]}``.
Used to seed the side-panel todo on attach/switch before the first
``todo_updated`` WS event arrives.
"""
client = _client()
resp = await client.get(f"/sessions/{session_id}/todos")
resp.raise_for_status()
return resp.json()
async def stop_session(session_id: str) -> dict:
client = _client()
resp = await client.post(f"/sessions/{session_id}/stop")
resp.raise_for_status()
return resp.json()