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