Skip to main content

atomr_agents_coding_cli_harness_web/
lib.rs

1//! Axum + embedded SPA companion for the coding-cli harness.
2//!
3//! Mirrors `atomr-agents-deep-research-harness-web` — same
4//! `WebServer` / `WebHandle` / `WebConfig` / `AppState` split — but
5//! adds a WebSocket route for the tmux-PTY bridge in interactive mode.
6
7#![forbid(unsafe_code)]
8
9pub mod error;
10pub mod routes;
11pub mod runner;
12pub mod spa;
13pub mod sse;
14pub mod ws;
15
16use std::net::SocketAddr;
17use std::sync::Arc;
18
19use atomr_agents_coding_cli_harness::CodingCliHarness;
20use axum::Router;
21use tokio::sync::oneshot;
22use tokio::task::JoinHandle;
23
24use crate::runner::RunSupervisor;
25
26#[derive(Clone, Debug)]
27pub struct WebConfig {
28    pub bind: SocketAddr,
29}
30
31impl Default for WebConfig {
32    fn default() -> Self {
33        Self {
34            // 7000 stt, 7100 meetings, 7200 deep-research → 7300.
35            bind: "127.0.0.1:7300".parse().expect("valid default addr"),
36        }
37    }
38}
39
40#[derive(Clone)]
41pub struct AppState {
42    pub harness: Arc<CodingCliHarness>,
43    pub supervisor: Arc<parking_lot::Mutex<RunSupervisor>>,
44}
45
46pub struct WebHandle {
47    pub bound_addr: SocketAddr,
48    shutdown_tx: Option<oneshot::Sender<()>>,
49    join: Option<JoinHandle<()>>,
50}
51
52impl WebHandle {
53    pub async fn shutdown(mut self) {
54        if let Some(tx) = self.shutdown_tx.take() {
55            let _ = tx.send(());
56        }
57        if let Some(join) = self.join.take() {
58            let _ = join.await;
59        }
60    }
61}
62
63pub struct WebServer {
64    config: WebConfig,
65    state: AppState,
66}
67
68impl WebServer {
69    pub fn new(config: WebConfig, harness: Arc<CodingCliHarness>) -> Self {
70        Self {
71            config,
72            state: AppState {
73                harness,
74                supervisor: Arc::new(parking_lot::Mutex::new(RunSupervisor::default())),
75            },
76        }
77    }
78
79    pub fn router(&self) -> Router {
80        routes::build_router(self.state.clone())
81    }
82
83    pub async fn start(self) -> Result<WebHandle, ServerError> {
84        let router = self.router();
85        let listener = tokio::net::TcpListener::bind(self.config.bind)
86            .await
87            .map_err(ServerError::Bind)?;
88        let bound_addr = listener.local_addr().map_err(ServerError::Bind)?;
89        let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
90        let join = tokio::spawn(async move {
91            let _ = axum::serve(listener, router.into_make_service())
92                .with_graceful_shutdown(async {
93                    let _ = shutdown_rx.await;
94                })
95                .await;
96        });
97        Ok(WebHandle {
98            bound_addr,
99            shutdown_tx: Some(shutdown_tx),
100            join: Some(join),
101        })
102    }
103}
104
105#[derive(Debug, thiserror::Error)]
106pub enum ServerError {
107    #[error("failed to bind: {0}")]
108    Bind(std::io::Error),
109}