Skip to main content

st/mcp/
dashboard_bridge.rs

1//! Dashboard Bridge - Connects MCP tool execution to real-time dashboard
2//!
3//! When the dashboard is running, this bridge pushes tool activity events
4//! to the shared state for visualization in the Wave Compass.
5
6use crate::web_dashboard::state_sync::{AccessType, McpActivityState, UserHint, UserHintsQueue};
7use std::path::PathBuf;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11/// Bridge between MCP context and Dashboard state
12#[derive(Clone)]
13pub struct DashboardBridge {
14    /// Shared MCP activity state (read by dashboard WebSocket)
15    activity: Arc<RwLock<McpActivityState>>,
16    /// Shared user hints queue (written by dashboard WebSocket)
17    hints: Arc<RwLock<UserHintsQueue>>,
18}
19
20impl DashboardBridge {
21    /// Create a new dashboard bridge with the given shared state handles
22    pub fn new(
23        activity: Arc<RwLock<McpActivityState>>,
24        hints: Arc<RwLock<UserHintsQueue>>,
25    ) -> Self {
26        Self { activity, hints }
27    }
28
29    /// Record the start of a tool execution
30    pub async fn tool_started(&self, tool_name: &str, parameters: serde_json::Value) {
31        let mut activity = self.activity.write().await;
32        activity.start_tool(tool_name, parameters);
33    }
34
35    /// Update tool progress (0.0 to 1.0)
36    pub async fn tool_progress(&self, progress: f32) {
37        let mut activity = self.activity.write().await;
38        activity.update_progress(progress);
39    }
40
41    /// Record a file access event
42    pub async fn file_accessed(&self, path: PathBuf, access_type: AccessType, tool_name: &str) {
43        let mut activity = self.activity.write().await;
44        activity.record_file_access(path, access_type, tool_name);
45    }
46
47    /// Record a file read
48    pub async fn file_read(&self, path: impl Into<PathBuf>, tool_name: &str) {
49        self.file_accessed(path.into(), AccessType::Read, tool_name)
50            .await;
51    }
52
53    /// Record a file write
54    pub async fn file_written(&self, path: impl Into<PathBuf>, tool_name: &str) {
55        self.file_accessed(path.into(), AccessType::Write, tool_name)
56            .await;
57    }
58
59    /// Record a file analysis (e.g., AST parsing)
60    pub async fn file_analyzed(&self, path: impl Into<PathBuf>, tool_name: &str) {
61        self.file_accessed(path.into(), AccessType::Analyze, tool_name)
62            .await;
63    }
64
65    /// Record a search operation on a path
66    pub async fn path_searched(&self, path: impl Into<PathBuf>, tool_name: &str) {
67        self.file_accessed(path.into(), AccessType::Search, tool_name)
68            .await;
69    }
70
71    /// Record tool completion
72    pub async fn tool_completed(&self, success: bool, summary: &str) {
73        let mut activity = self.activity.write().await;
74        activity.complete_tool(success, summary);
75    }
76
77    /// Update the current operation description
78    pub async fn set_operation(&self, operation: &str) {
79        let mut activity = self.activity.write().await;
80        activity.current_operation = operation.to_string();
81    }
82
83    /// Get and consume the next unconsumed user hint
84    pub async fn consume_hint(&self) -> Option<UserHint> {
85        let mut hints = self.hints.write().await;
86        hints.consume_next()
87    }
88
89    /// Peek at unconsumed hints without consuming them
90    pub async fn peek_hints(&self) -> Vec<UserHint> {
91        let hints = self.hints.read().await;
92        hints.peek_unconsumed().into_iter().cloned().collect()
93    }
94
95    /// Check if there are any pending hints
96    pub async fn has_pending_hints(&self) -> bool {
97        let hints = self.hints.read().await;
98        hints.unconsumed_count() > 0
99    }
100
101    /// Get the current MCP activity state for display
102    pub async fn get_activity_snapshot(&self) -> McpActivitySnapshot {
103        let activity = self.activity.read().await;
104        McpActivitySnapshot {
105            active_tool: activity.active_tool.as_ref().map(|t| t.name.clone()),
106            current_operation: activity.current_operation.clone(),
107            files_touched_count: activity.files_touched.len(),
108            directories_explored_count: activity.directories_explored.len(),
109            tools_executed_count: activity.tool_history.len(),
110        }
111    }
112}
113
114/// Lightweight snapshot of MCP activity for logging/display
115#[derive(Debug, Clone)]
116pub struct McpActivitySnapshot {
117    pub active_tool: Option<String>,
118    pub current_operation: String,
119    pub files_touched_count: usize,
120    pub directories_explored_count: usize,
121    pub tools_executed_count: usize,
122}
123
124/// Macro to wrap tool execution with dashboard bridge reporting
125#[macro_export]
126macro_rules! with_dashboard_bridge {
127    ($ctx:expr, $tool_name:expr, $params:expr, $body:expr) => {{
128        // Report tool start if dashboard is connected
129        if let Some(ref bridge) = $ctx.dashboard_bridge {
130            bridge.tool_started($tool_name, $params.clone()).await;
131        }
132
133        // Execute the tool
134        let result = $body;
135
136        // Report completion
137        if let Some(ref bridge) = $ctx.dashboard_bridge {
138            match &result {
139                Ok(val) => {
140                    let summary = match val.as_str() {
141                        Some(s) if s.len() > 100 => format!("{}...", &s[..100]),
142                        Some(s) => s.to_string(),
143                        None => "OK".to_string(),
144                    };
145                    bridge.tool_completed(true, &summary).await;
146                }
147                Err(e) => {
148                    bridge.tool_completed(false, &e.to_string()).await;
149                }
150            }
151        }
152
153        result
154    }};
155}