Skip to main content

agent_client_protocol_trace_viewer/
lib.rs

1//! Agent Client Protocol Trace Viewer Library
2//!
3//! Provides an interactive sequence diagram viewer for ACP trace events.
4//! Can serve events from memory (for live viewing) or from a file.
5
6use std::path::PathBuf;
7use std::sync::{Arc, Mutex};
8
9use axum::{
10    Router,
11    extract::State,
12    http::StatusCode,
13    response::{Html, IntoResponse, Response},
14    routing::get,
15};
16use tokio::net::TcpListener;
17
18/// The HTML viewer page (embedded at compile time).
19pub const VIEWER_HTML: &str = include_str!("viewer.html");
20
21/// Source of trace events for the viewer.
22#[derive(Debug, Clone)]
23pub enum TraceSource {
24    /// Read events from a file (re-reads on each request for live updates).
25    File(PathBuf),
26    /// Read events from shared memory.
27    Memory(Arc<Mutex<Vec<serde_json::Value>>>),
28}
29
30/// Handle to push events when using memory-backed trace source.
31#[derive(Debug, Clone)]
32pub struct TraceHandle {
33    events: Arc<Mutex<Vec<serde_json::Value>>>,
34}
35
36impl TraceHandle {
37    /// Push a new event to the trace.
38    pub fn push(&self, event: serde_json::Value) {
39        self.events
40            .lock()
41            .expect("events mutex poisoned")
42            .push(event);
43    }
44
45    /// Get the current number of events.
46    #[must_use]
47    pub fn len(&self) -> usize {
48        self.events.lock().expect("events mutex poisoned").len()
49    }
50
51    /// Check if empty.
52    #[must_use]
53    pub fn is_empty(&self) -> bool {
54        self.events
55            .lock()
56            .expect("events mutex poisoned")
57            .is_empty()
58    }
59}
60
61struct AppState {
62    source: TraceSource,
63}
64
65/// Configuration for the trace viewer server.
66#[derive(Debug)]
67pub struct TraceViewerConfig {
68    /// Port to serve on (0 = auto-select).
69    pub port: u16,
70    /// Whether to open browser automatically.
71    pub open_browser: bool,
72}
73
74impl Default for TraceViewerConfig {
75    fn default() -> Self {
76        Self {
77            port: 0,
78            open_browser: true,
79        }
80    }
81}
82
83/// Start the trace viewer server with a memory-backed event source.
84///
85/// Returns a handle to push events and a future that runs the server.
86/// The server will poll for new events automatically.
87///
88/// # Example
89///
90/// ```no_run
91/// # async fn example() -> anyhow::Result<()> {
92/// let (handle, server) = agent_client_protocol_trace_viewer::serve_memory(Default::default())?;
93///
94/// // Push events from your application
95/// handle.push(serde_json::json!({"type": "request", "method": "test"}));
96///
97/// // Run the server (or spawn it)
98/// server.await?;
99/// # Ok(())
100/// # }
101/// ```
102pub fn serve_memory(
103    config: TraceViewerConfig,
104) -> anyhow::Result<(
105    TraceHandle,
106    impl std::future::Future<Output = anyhow::Result<()>>,
107)> {
108    let events = Arc::new(Mutex::new(Vec::new()));
109    let handle = TraceHandle {
110        events: events.clone(),
111    };
112    let source = TraceSource::Memory(events);
113
114    let server = serve_impl(source, config);
115    Ok((handle, server))
116}
117
118/// Start the trace viewer server with a file-backed event source.
119///
120/// The file is re-read on each request, allowing live updates as the file grows.
121///
122/// # Example
123///
124/// ```no_run
125/// # use std::path::PathBuf;
126/// # async fn example() -> anyhow::Result<()> {
127/// agent_client_protocol_trace_viewer::serve_file(PathBuf::from("trace.jsons"), Default::default()).await?;
128/// # Ok(())
129/// # }
130/// ```
131pub async fn serve_file(path: PathBuf, config: TraceViewerConfig) -> anyhow::Result<()> {
132    serve_impl(TraceSource::File(path), config).await
133}
134
135async fn serve_impl(source: TraceSource, config: TraceViewerConfig) -> anyhow::Result<()> {
136    let state = Arc::new(AppState { source });
137
138    let app = Router::new()
139        .route("/", get(serve_viewer))
140        .route("/events", get(serve_events))
141        .with_state(state);
142
143    let listener = TcpListener::bind(format!("127.0.0.1:{}", config.port)).await?;
144    let addr = listener.local_addr()?;
145
146    eprintln!("Trace viewer at http://{addr}");
147
148    if config.open_browser {
149        let url = format!("http://{addr}");
150        if let Err(e) = open::that(&url) {
151            eprintln!("Failed to open browser: {e}. Open {url} manually.");
152        }
153    }
154
155    axum::serve(listener, app).await?;
156    Ok(())
157}
158
159/// Serve the main viewer HTML page.
160async fn serve_viewer() -> Html<&'static str> {
161    Html(VIEWER_HTML)
162}
163
164/// Serve the trace events as JSON.
165async fn serve_events(State(state): State<Arc<AppState>>) -> Response {
166    match &state.source {
167        TraceSource::File(path) => serve_events_from_file(path).await,
168        TraceSource::Memory(events) => serve_events_from_memory(events),
169    }
170}
171
172async fn serve_events_from_file(path: &PathBuf) -> Response {
173    match tokio::fs::read_to_string(path).await {
174        Ok(content) => {
175            let events: Vec<serde_json::Value> = content
176                .lines()
177                .filter(|line| !line.trim().is_empty())
178                .filter_map(|line| serde_json::from_str(line).ok())
179                .collect();
180
181            match serde_json::to_string(&events) {
182                Ok(json) => {
183                    (StatusCode::OK, [("content-type", "application/json")], json).into_response()
184                }
185                Err(e) => (
186                    StatusCode::INTERNAL_SERVER_ERROR,
187                    format!("Failed to serialize events: {e}"),
188                )
189                    .into_response(),
190            }
191        }
192        Err(e) => (
193            StatusCode::INTERNAL_SERVER_ERROR,
194            format!("Failed to read trace file: {e}"),
195        )
196            .into_response(),
197    }
198}
199
200fn serve_events_from_memory(events: &Arc<Mutex<Vec<serde_json::Value>>>) -> Response {
201    let events = events.lock().expect("events mutex poisoned");
202    match serde_json::to_string(&*events) {
203        Ok(json) => (StatusCode::OK, [("content-type", "application/json")], json).into_response(),
204        Err(e) => (
205            StatusCode::INTERNAL_SERVER_ERROR,
206            format!("Failed to serialize events: {e}"),
207        )
208            .into_response(),
209    }
210}