Skip to main content

atomr_agents_coding_cli_harness_web/
routes.rs

1//! REST + SSE + WS route registration.
2
3use std::sync::Arc;
4
5use atomr_agents_coding_cli_core::{CliRequest, CliRunId, CliSessionId};
6use axum::extract::{Path, State};
7use axum::http::StatusCode;
8use axum::response::IntoResponse;
9use axum::routing::{delete, get};
10use axum::{Json, Router};
11use serde_json::json;
12use tower_http::cors::CorsLayer;
13
14use crate::error::WebError;
15use crate::AppState;
16
17pub fn build_router(state: AppState) -> Router {
18    let api = Router::new()
19        .route("/cli/vendors", get(list_vendors))
20        .route("/cli/runs", get(list_runs).post(start_run))
21        .route("/cli/runs/:id", get(get_run))
22        .route("/cli/runs/events", get(crate::sse::sse_events))
23        .route(
24            "/cli/sessions",
25            get(list_sessions).post(start_session),
26        )
27        .route("/cli/sessions/:id", delete(stop_session))
28        .route("/cli/sessions/:id/io", get(crate::ws::session_ws))
29        .with_state(state.clone());
30
31    Router::new()
32        .nest("/api", api)
33        .route("/healthz", get(|| async { "ok" }))
34        .fallback(crate::spa::serve_embedded)
35        .with_state(state)
36        .layer(CorsLayer::permissive())
37}
38
39// ----- handlers ---------------------------------------------------------
40
41async fn list_vendors(State(state): State<AppState>) -> impl IntoResponse {
42    let kinds: Vec<_> = state
43        .harness
44        .available_vendors()
45        .into_iter()
46        .map(|k| k.as_str().to_string())
47        .collect();
48    Json(json!({ "vendors": kinds }))
49}
50
51async fn start_run(
52    State(state): State<AppState>,
53    Json(req): Json<CliRequest>,
54) -> Result<impl IntoResponse, WebError> {
55    let harness = state.harness.clone();
56    let run_id = CliRunId::new();
57    let id_clone = run_id.clone();
58    // Spawn so the HTTP request returns immediately. Errors land in
59    // tracing + the run's store entry.
60    let task = tokio::spawn(async move {
61        if let Err(e) = harness.run(req).await {
62            tracing::error!(run_id = %id_clone, error = %e, "headless run failed");
63        }
64    });
65    state.supervisor.lock().register(run_id.clone(), task);
66    Ok((StatusCode::ACCEPTED, Json(json!({ "run_id": run_id }))))
67}
68
69async fn list_runs(State(state): State<AppState>) -> Result<impl IntoResponse, WebError> {
70    let runs = state.harness.store.list(50).await?;
71    Ok(Json(json!({ "runs": runs })))
72}
73
74async fn get_run(
75    State(state): State<AppState>,
76    Path(id): Path<String>,
77) -> Result<impl IntoResponse, WebError> {
78    let id = CliRunId::from(id);
79    match state.harness.store.get(&id).await? {
80        Some(r) => Ok(Json(r).into_response()),
81        None => Ok((StatusCode::NOT_FOUND, Json(json!({"error":"run not found"}))).into_response()),
82    }
83}
84
85async fn start_session(
86    State(state): State<AppState>,
87    Json(req): Json<CliRequest>,
88) -> Result<impl IntoResponse, WebError> {
89    let handle = state.harness.start_interactive(req).await?;
90    Ok((
91        StatusCode::ACCEPTED,
92        Json(json!({
93            "session_id": handle.id,
94            "vendor": handle.vendor.as_str(),
95            "tmux_session": handle.tmux_session,
96            "started_at": handle.started_at,
97        })),
98    ))
99}
100
101async fn list_sessions(State(state): State<AppState>) -> impl IntoResponse {
102    let sessions: Vec<_> = state
103        .harness
104        .sessions()
105        .list()
106        .into_iter()
107        .map(|h: Arc<atomr_agents_coding_cli_harness::InteractiveSessionHandle>| {
108            json!({
109                "id": h.id,
110                "vendor": h.vendor.as_str(),
111                "tmux_session": h.tmux_session,
112                "started_at": h.started_at,
113                "closed": *h.closed.lock(),
114            })
115        })
116        .collect();
117    Json(json!({ "sessions": sessions }))
118}
119
120async fn stop_session(
121    State(state): State<AppState>,
122    Path(id): Path<String>,
123) -> Result<impl IntoResponse, WebError> {
124    let id = CliSessionId::from(id);
125    state.harness.stop_interactive(&id).await?;
126    Ok((StatusCode::NO_CONTENT, ()))
127}