Skip to main content

cinch_web/
lib.rs

1//! Browser-based chat UI for cinch-rs powered agents.
2//!
3//! `cinch-web` provides an axum web server that exposes a WebSocket endpoint
4//! for real-time agent observation and a REST API for control. It is designed
5//! to be paired with a Next.js 16 frontend but works with any WebSocket client.
6//!
7//! # Quick start
8//!
9//! ```ignore
10//! use cinch_web::{WebConfig, spawn_web};
11//! use cinch_rs::ui::UiState;
12//! use std::sync::{Arc, Mutex};
13//!
14//! let ui_state = Arc::new(Mutex::new(UiState::default()));
15//! let (ws_tx, _) = tokio::sync::broadcast::channel(256);
16//!
17//! let config = WebConfig::default();
18//! let (addr, chat_rx) = spawn_web(ui_state, ws_tx, config).await;
19//! println!("Web UI: http://{addr}");
20//!
21//! // Read user messages sent from the browser:
22//! while let Some(msg) = chat_rx.recv().await {
23//!     println!("User said: {msg}");
24//! }
25//! ```
26//!
27//! # Architecture
28//!
29//! ```text
30//! Agent runtime ──HarnessEvent──▶ WebBroadcastHandler ──WsMessage──▶ WebSocket clients
31//!                                                                         ▲
32//!           Arc<Mutex<UiState>> ◀── /api/answer, /api/control ────────────┘
33//! ```
34//!
35//! The [`WebBroadcastHandler`] implements [`EventHandler`](cinch_rs::agent::events::EventHandler)
36//! and converts harness events into serialized WebSocket messages. Compose it
37//! alongside [`UiEventHandler`](cinch_rs::ui::event_handler::UiEventHandler)
38//! in a [`CompositeEventHandler`](cinch_rs::agent::CompositeEventHandler).
39
40mod api;
41pub mod broadcast;
42pub mod ext;
43mod server;
44pub mod snapshot;
45mod ws;
46
47pub use broadcast::{WebBroadcastHandler, WsMessage};
48pub use ext::{ChoiceMetadata, NoWebExtension, StatusField, WebExtensionRenderer};
49pub use snapshot::UiStateSnapshot;
50
51use std::net::SocketAddr;
52use std::path::PathBuf;
53use std::sync::{Arc, Mutex};
54
55use cinch_rs::ui::UiState;
56
57/// Configuration for the web server.
58pub struct WebConfig {
59    /// Address to bind to. Default: `127.0.0.1:3001`.
60    pub bind_addr: SocketAddr,
61    /// Path to the Next.js static export directory (for production mode).
62    ///
63    /// If `None`, only API/WS endpoints are served — the frontend runs
64    /// separately (e.g., `next dev` on port 3000).
65    pub static_dir: Option<PathBuf>,
66    /// Maximum WebSocket broadcast channel capacity. Default: 256.
67    ///
68    /// Clients that fall behind by this many messages receive a fresh
69    /// state snapshot to resynchronize.
70    pub broadcast_capacity: usize,
71}
72
73impl Default for WebConfig {
74    fn default() -> Self {
75        Self {
76            bind_addr: SocketAddr::from(([127, 0, 0, 1], 3001)),
77            static_dir: None,
78            broadcast_capacity: 256,
79        }
80    }
81}
82
83/// Spawn the web server on a Tokio task.
84///
85/// Returns the bound address and a receiver for chat messages sent from the
86/// browser (via `POST /api/chat` or `{"type":"chat"}` WebSocket messages).
87/// Read from the receiver in your agent loop to drive conversation turns.
88///
89/// The server runs until the Tokio runtime shuts down.
90///
91/// # Arguments
92///
93/// * `ui_state` — Shared agent state (same instance passed to `UiEventHandler`).
94/// * `broadcast_tx` — Sender half of the WebSocket broadcast channel. Pass the
95///   same sender to [`WebBroadcastHandler::new()`].
96/// * `config` — Server configuration.
97pub async fn spawn_web(
98    ui_state: Arc<Mutex<UiState>>,
99    broadcast_tx: tokio::sync::broadcast::Sender<WsMessage>,
100    config: WebConfig,
101) -> (SocketAddr, tokio::sync::mpsc::Receiver<String>) {
102    let (chat_tx, chat_rx) = tokio::sync::mpsc::channel(32);
103    let router = server::build_router(ui_state, broadcast_tx, chat_tx, config.static_dir);
104    let addr = server::start_server(router, config.bind_addr).await;
105    (addr, chat_rx)
106}