atomr_agents_coding_cli_harness_web/
routes.rs1use 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
39async 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 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}