|
4 | 4 | //! Resources are read-only state that agents can query. |
5 | 5 |
|
6 | 6 | use serde::{Deserialize, Serialize}; |
7 | | -use tauri::{Manager, Runtime, WebviewWindow}; |
| 7 | +use serde_json::{Value, json}; |
| 8 | +use tauri::{Emitter, Listener, Manager, Runtime, WebviewWindow}; |
8 | 9 |
|
9 | 10 | use super::dialog_state::SoftDialogTracker; |
10 | 11 | use super::pane_state::{FileEntry, PaneState, PaneStateStore, TabInfo}; |
@@ -52,6 +53,12 @@ pub fn get_all_resources() -> Vec<Resource> { |
52 | 53 | description: "Current drive indexing phase, timeline history, and database stats".to_string(), |
53 | 54 | mime_type: "text/plain".to_string(), |
54 | 55 | }, |
| 56 | + Resource { |
| 57 | + uri: "cmdr://settings".to_string(), |
| 58 | + name: "Settings".to_string(), |
| 59 | + description: "All settings with current values, defaults, types, and constraints".to_string(), |
| 60 | + mime_type: "text/yaml".to_string(), |
| 61 | + }, |
55 | 62 | ] |
56 | 63 | } |
57 | 64 |
|
@@ -236,8 +243,57 @@ fn build_available_dialogs_yaml<R: Runtime>(app: &tauri::AppHandle<R>) -> String |
236 | 243 | yaml |
237 | 244 | } |
238 | 245 |
|
| 246 | +/// Emit an event to the frontend and wait for a response containing data. |
| 247 | +/// |
| 248 | +/// Similar to `mcp_round_trip` in executor.rs, but returns the `data` field from the response |
| 249 | +/// instead of a fixed success message. The frontend must emit `mcp-response` with |
| 250 | +/// `{ requestId, ok, data?, error? }`. Times out after 5 seconds. |
| 251 | +async fn resource_round_trip<R: Runtime>( |
| 252 | + app: &tauri::AppHandle<R>, |
| 253 | + event: &str, |
| 254 | + mut payload: Value, |
| 255 | +) -> Result<String, String> { |
| 256 | + let request_id = uuid::Uuid::new_v4().to_string(); |
| 257 | + payload["requestId"] = json!(request_id); |
| 258 | + |
| 259 | + let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>(); |
| 260 | + let expected_id = request_id.clone(); |
| 261 | + |
| 262 | + let tx = std::sync::Mutex::new(Some(tx)); |
| 263 | + let listener_id = app.listen("mcp-response", move |event| { |
| 264 | + if let Ok(resp) = serde_json::from_str::<Value>(event.payload()) |
| 265 | + && resp.get("requestId").and_then(|v| v.as_str()) == Some(&expected_id) |
| 266 | + && let Some(tx) = tx.lock().unwrap().take() |
| 267 | + { |
| 268 | + let result = if resp.get("ok").and_then(|v| v.as_bool()).unwrap_or(false) { |
| 269 | + let data = resp.get("data").and_then(|v| v.as_str()).unwrap_or("").to_string(); |
| 270 | + Ok(data) |
| 271 | + } else { |
| 272 | + let err = resp |
| 273 | + .get("error") |
| 274 | + .and_then(|v| v.as_str()) |
| 275 | + .unwrap_or("Unknown error") |
| 276 | + .to_string(); |
| 277 | + Err(err) |
| 278 | + }; |
| 279 | + let _ = tx.send(result); |
| 280 | + } |
| 281 | + }); |
| 282 | + |
| 283 | + app.emit(event, payload).map_err(|e| e.to_string())?; |
| 284 | + |
| 285 | + let result = tokio::time::timeout(std::time::Duration::from_secs(5), rx).await; |
| 286 | + app.unlisten(listener_id); |
| 287 | + |
| 288 | + match result { |
| 289 | + Ok(Ok(data)) => data, |
| 290 | + Ok(Err(_)) => Err("Frontend response channel dropped".to_string()), |
| 291 | + Err(_) => Err("Frontend did not respond within 5 seconds".to_string()), |
| 292 | + } |
| 293 | +} |
| 294 | + |
239 | 295 | /// Read a resource by URI. |
240 | | -pub fn read_resource<R: Runtime>(app: &tauri::AppHandle<R>, uri: &str) -> Result<ResourceContent, String> { |
| 296 | +pub async fn read_resource<R: Runtime>(app: &tauri::AppHandle<R>, uri: &str) -> Result<ResourceContent, String> { |
241 | 297 | let (content, mime_type) = match uri { |
242 | 298 | "cmdr://state" => { |
243 | 299 | let store = app.try_state::<PaneStateStore>().ok_or("Pane state not available")?; |
@@ -324,6 +380,10 @@ pub fn read_resource<R: Runtime>(app: &tauri::AppHandle<R>, uri: &str) -> Result |
324 | 380 | let text = build_indexing_status_text(); |
325 | 381 | (text, "text/plain") |
326 | 382 | } |
| 383 | + "cmdr://settings" => { |
| 384 | + let text = resource_round_trip(app, "mcp-get-all-settings", json!({})).await?; |
| 385 | + (text, "text/yaml") |
| 386 | + } |
327 | 387 | _ => return Err(format!("Unknown resource URI: {}", uri)), |
328 | 388 | }; |
329 | 389 |
|
@@ -472,7 +532,7 @@ mod tests { |
472 | 532 | #[test] |
473 | 533 | fn test_resource_count() { |
474 | 534 | let resources = get_all_resources(); |
475 | | - assert_eq!(resources.len(), 3); |
| 535 | + assert_eq!(resources.len(), 4); |
476 | 536 | } |
477 | 537 |
|
478 | 538 | #[test] |
|
0 commit comments