Skip to content

Commit 1061fad

Browse files
committed
MCP rewrite
Found that the old MCP server I designed was awkward, not flexible enough, while it had too many tools. The new structure has less tools but more capabilities. See agent-centric-mcp-plan.md for the details.
1 parent 1a37b35 commit 1061fad

23 files changed

Lines changed: 2606 additions & 1150 deletions

apps/desktop/src-tauri/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ use mtp_rs as _;
4747
// nusb is used in mtp/watcher.rs for USB hotplug detection
4848
#[cfg(target_os = "macos")]
4949
use nusb as _;
50+
//noinspection ALL
51+
// objc2-app-kit is used in volumes/mod.rs for NSRunningApplication
52+
#[cfg(target_os = "macos")]
53+
use objc2_app_kit as _;
5054

5155
mod ai;
5256
pub mod benchmark;
@@ -180,6 +184,9 @@ pub fn run() {
180184
// Initialize pane state store for MCP context tools
181185
app.manage(mcp::PaneStateStore::new());
182186

187+
// Initialize dialog state store for MCP dialog tracking
188+
app.manage(mcp::DialogStateStore::new());
189+
183190
// Initialize settings state store for MCP settings tools
184191
app.manage(mcp::SettingsStateStore::new());
185192

@@ -352,6 +359,7 @@ pub fn run() {
352359
mcp::pane_state::update_left_pane_state,
353360
mcp::pane_state::update_right_pane_state,
354361
mcp::pane_state::update_focused_pane,
362+
mcp::dialog_state::update_dialog_state,
355363
mcp::settings_state::mcp_update_settings_state,
356364
mcp::settings_state::mcp_update_settings_open,
357365
mcp::settings_state::mcp_update_settings_section,
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//! Dialog state storage for MCP context tools.
2+
//!
3+
//! Stores the current state of open dialogs so MCP tools can query it.
4+
5+
use serde::{Deserialize, Serialize};
6+
use std::sync::RwLock;
7+
use tauri::{AppHandle, Manager};
8+
9+
/// Represents an open file viewer dialog.
10+
#[derive(Debug, Clone, Serialize, Deserialize)]
11+
#[serde(rename_all = "camelCase")]
12+
pub struct FileViewerEntry {
13+
pub path: String,
14+
}
15+
16+
/// State of open dialogs.
17+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
18+
#[serde(rename_all = "camelCase")]
19+
pub struct DialogState {
20+
/// Whether the settings dialog is open
21+
pub settings_open: bool,
22+
/// Whether the about dialog is open
23+
pub about_open: bool,
24+
/// Whether the volume picker is open
25+
pub volume_picker_open: bool,
26+
/// Whether a confirmation dialog is open
27+
pub confirmation_open: bool,
28+
/// Open file viewer dialogs (can have multiple)
29+
pub file_viewers: Vec<FileViewerEntry>,
30+
}
31+
32+
/// Shared state for dialog tracking.
33+
#[derive(Debug, Default)]
34+
pub struct DialogStateStore {
35+
state: RwLock<DialogState>,
36+
}
37+
38+
impl DialogStateStore {
39+
pub fn new() -> Self {
40+
Self {
41+
state: RwLock::new(DialogState::default()),
42+
}
43+
}
44+
45+
pub fn get(&self) -> DialogState {
46+
self.state.read().unwrap().clone()
47+
}
48+
49+
pub fn set_settings_open(&self, open: bool) {
50+
self.state.write().unwrap().settings_open = open;
51+
}
52+
53+
pub fn set_about_open(&self, open: bool) {
54+
self.state.write().unwrap().about_open = open;
55+
}
56+
57+
pub fn set_volume_picker_open(&self, open: bool) {
58+
self.state.write().unwrap().volume_picker_open = open;
59+
}
60+
61+
pub fn set_confirmation_open(&self, open: bool) {
62+
self.state.write().unwrap().confirmation_open = open;
63+
}
64+
65+
pub fn add_file_viewer(&self, path: String) {
66+
let mut state = self.state.write().unwrap();
67+
// Don't add duplicates
68+
if !state.file_viewers.iter().any(|v| v.path == path) {
69+
state.file_viewers.push(FileViewerEntry { path });
70+
}
71+
}
72+
73+
pub fn remove_file_viewer(&self, path: &str) {
74+
let mut state = self.state.write().unwrap();
75+
state.file_viewers.retain(|v| v.path != path);
76+
}
77+
78+
pub fn clear_all_file_viewers(&self) {
79+
self.state.write().unwrap().file_viewers.clear();
80+
}
81+
}
82+
83+
/// Tauri command to update dialog state from frontend.
84+
#[tauri::command]
85+
pub fn update_dialog_state(app: AppHandle, dialog_type: String, action: String, path: Option<String>) {
86+
if let Some(store) = app.try_state::<DialogStateStore>() {
87+
match (dialog_type.as_str(), action.as_str()) {
88+
("settings", "open") => store.set_settings_open(true),
89+
("settings", "close") => store.set_settings_open(false),
90+
("about", "open") => store.set_about_open(true),
91+
("about", "close") => store.set_about_open(false),
92+
("volume-picker", "open") => store.set_volume_picker_open(true),
93+
("volume-picker", "close") => store.set_volume_picker_open(false),
94+
("confirmation", "open") => store.set_confirmation_open(true),
95+
("confirmation", "close") => store.set_confirmation_open(false),
96+
("file-viewer", "open") => {
97+
if let Some(p) = path {
98+
store.add_file_viewer(p);
99+
}
100+
}
101+
("file-viewer", "close") => {
102+
if let Some(p) = path {
103+
store.remove_file_viewer(&p);
104+
}
105+
}
106+
("file-viewer", "close-all") => store.clear_all_file_viewers(),
107+
_ => {}
108+
}
109+
}
110+
}
111+
112+
#[cfg(test)]
113+
mod tests {
114+
use super::*;
115+
116+
#[test]
117+
fn test_dialog_state_store() {
118+
let store = DialogStateStore::new();
119+
120+
// Initial state should be all closed
121+
let state = store.get();
122+
assert!(!state.settings_open);
123+
assert!(!state.about_open);
124+
assert!(!state.volume_picker_open);
125+
assert!(!state.confirmation_open);
126+
assert!(state.file_viewers.is_empty());
127+
128+
// Open settings
129+
store.set_settings_open(true);
130+
assert!(store.get().settings_open);
131+
132+
// Close settings
133+
store.set_settings_open(false);
134+
assert!(!store.get().settings_open);
135+
}
136+
137+
#[test]
138+
fn test_file_viewers() {
139+
let store = DialogStateStore::new();
140+
141+
// Add a file viewer
142+
store.add_file_viewer("/path/to/file.txt".to_string());
143+
assert_eq!(store.get().file_viewers.len(), 1);
144+
assert_eq!(store.get().file_viewers[0].path, "/path/to/file.txt");
145+
146+
// Don't add duplicates
147+
store.add_file_viewer("/path/to/file.txt".to_string());
148+
assert_eq!(store.get().file_viewers.len(), 1);
149+
150+
// Add another file viewer
151+
store.add_file_viewer("/path/to/other.txt".to_string());
152+
assert_eq!(store.get().file_viewers.len(), 2);
153+
154+
// Remove one
155+
store.remove_file_viewer("/path/to/file.txt");
156+
assert_eq!(store.get().file_viewers.len(), 1);
157+
assert_eq!(store.get().file_viewers[0].path, "/path/to/other.txt");
158+
159+
// Clear all
160+
store.add_file_viewer("/path/to/file.txt".to_string());
161+
store.clear_all_file_viewers();
162+
assert!(store.get().file_viewers.is_empty());
163+
}
164+
}

0 commit comments

Comments
 (0)