feat(embedding,vision): candle embeddings with offline fallback, on-demand clipboard vision capture, and concurrency audit

This commit is contained in:
Riz Ashraf committed 2026-10-07 06:36:09 +01:00
1 parent 5bd8b1587a
commit e4a0fe72df
47 files changed
+6292 -3503

No files matched your search

+279 -84
View File
@@ -15,7 +15,10 @@ impl McpTool for PinFileHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<PinFileTool>("pin_file", "Pin a file path in the workspace context to prioritize it across subagent turns.")
crate::mcp::tool_def::<PinFileTool>(
"pin_file",
"Pin a file path in the workspace context to prioritize it across subagent turns.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -27,6 +30,9 @@ impl McpTool for PinFileHandler {
file_path: req.file_path,
timestamp: crate::handlers::utils::now_secs(),
git_branch: req.git_branch,
repo_name: req.repo_name,
symbol_or_line: req.symbol_or_line,
pin_reason: req.pin_reason,
});
});
Ok("File pinned".to_string())
@@ -42,7 +48,10 @@ impl McpTool for UnpinFileHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<UnpinFileTool>("unpin_file", "Unpin a file path from the workspace context.")
crate::mcp::tool_def::<UnpinFileTool>(
"unpin_file",
"Unpin a file path from the workspace context.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -104,7 +113,10 @@ impl McpTool for StoreSnippetHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<StoreSnippetTool>("store_snippet", "Store or replace a reusable code snippet with vector embedding and search tags.")
crate::mcp::tool_def::<StoreSnippetTool>(
"store_snippet",
"Store or replace a reusable code snippet with vector embedding and search tags.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -125,6 +137,12 @@ impl McpTool for StoreSnippetHandler {
updated_at: crate::handlers::utils::now_secs(),
tags: req.tags.unwrap_or_default(),
embedding,
namespace: req
.namespace
.unwrap_or_else(|| crate::models::default_namespace()),
repo_name: req.repo_name,
origin_file: req.origin_file,
line_range: req.line_range,
};
let idx = state.get_search_index().await;
@@ -148,7 +166,10 @@ impl McpTool for TagSnippetHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<TagSnippetTool>("tag_snippet", "Add organizational tags to an existing stored code snippet.")
crate::mcp::tool_def::<TagSnippetTool>(
"tag_snippet",
"Add organizational tags to an existing stored code snippet.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -169,9 +190,15 @@ impl McpTool for TagSnippetHandler {
});
if found {
Ok(format!("Snippet '{}' tagged with: {:?}", req.name, req.tags))
Ok(format!(
"Snippet '{}' tagged with: {:?}",
req.name, req.tags
))
} else {
Err(crate::error::AppError::Internal(format!("Snippet '{}' not found.", req.name)))
Err(crate::error::AppError::Internal(format!(
"Snippet '{}' not found.",
req.name
)))
}
}
}
@@ -209,7 +236,9 @@ impl McpTool for SearchSnippetsHandler {
|| s.tags.iter().any(|t| contains_ignore_ascii_case(t, &query));
let matches_tags = tags_filter.is_empty()
|| tags_filter.iter().any(|tf| s.tags.iter().any(|st| contains_ignore_ascii_case(st, tf)));
|| tags_filter
.iter()
.any(|tf| s.tags.iter().any(|st| contains_ignore_ascii_case(st, tf)));
matches_text && matches_tags
})
@@ -292,6 +321,10 @@ impl McpTool for SaveContextWorkspaceHandler {
pinned_files: req.pinned_files,
active_task_ids: req.active_task_ids,
saved_at: crate::handlers::utils::now_secs(),
description: req.description,
repo_name: req.repo_name,
git_branch: req.git_branch,
vcs_revision: req.vcs_revision,
});
});
Ok("Context workspace saved".to_string())
@@ -419,6 +452,10 @@ impl McpTool for AddPrChecklistItemHandler {
namespace: req.namespace,
id: uuid::Uuid::new_v4().to_string(),
description: req.description,
is_checked: req.is_checked.unwrap_or(false),
category: req.category,
repo_name: req.repo_name,
automated_check: req.automated_check,
})
});
Ok("PR checklist item added".to_string())
@@ -492,22 +529,43 @@ impl McpTool for PinnedFilesHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: PinnedFilesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
let ns = req
.namespace
.unwrap_or_else(|| crate::models::default_namespace());
match req.action {
PinnedFileAction::Pin => {
let path = req.path.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'path' for action 'pin'. Next step: Provide absolute or relative file 'path' in request and retry.".to_string())
})?;
PinFileHandler.execute(serde_json::json!({"file_path": path, "namespace": ns}), state).await
PinFileHandler
.execute(
serde_json::json!({
"file_path": path,
"namespace": ns,
"git_branch": req.git_branch,
"pin_reason": req.pin_reason,
"symbol_or_line": req.symbol_or_line,
"repo_name": req.repo_name,
}),
state,
)
.await
}
PinnedFileAction::Unpin => {
let path = req.path.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'path' for action 'unpin'. Next step: Provide file 'path' in request and retry.".to_string())
})?;
UnpinFileHandler.execute(serde_json::json!({"file_path": path, "namespace": ns}), state).await
UnpinFileHandler
.execute(
serde_json::json!({"file_path": path, "namespace": ns}),
state,
)
.await
}
PinnedFileAction::List => {
ListPinnedFilesHandler.execute(serde_json::json!({"namespace": ns}), state).await
ListPinnedFilesHandler
.execute(serde_json::json!({"namespace": ns}), state)
.await
}
}
}
@@ -530,7 +588,9 @@ impl McpTool for ContextWorkspacesHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ContextWorkspacesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
let ns = req
.namespace
.unwrap_or_else(|| crate::models::default_namespace());
match req.action {
ContextWorkspaceAction::Save => {
let name = req.name.ok_or_else(|| {
@@ -538,30 +598,42 @@ impl McpTool for ContextWorkspacesHandler {
})?;
let pinned_files = req.pinned_files.unwrap_or_default();
let active_task_ids = req.active_task_ids.unwrap_or_default();
SaveContextWorkspaceHandler.execute(
serde_json::json!({
"name": name,
"namespace": ns,
"pinned_files": pinned_files,
"active_task_ids": active_task_ids
}),
state,
).await
SaveContextWorkspaceHandler
.execute(
serde_json::json!({
"name": name,
"namespace": ns,
"pinned_files": pinned_files,
"active_task_ids": active_task_ids,
"description": req.description,
"repo_name": req.repo_name,
"git_branch": req.git_branch,
"vcs_revision": req.vcs_revision,
}),
state,
)
.await
}
ContextWorkspaceAction::Load => {
let name = req.name.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'name' for action 'load'. Next step: Provide workspace 'name' string in request and retry.".to_string())
})?;
LoadContextWorkspaceHandler.execute(serde_json::json!({"name": name, "namespace": ns}), state).await
LoadContextWorkspaceHandler
.execute(serde_json::json!({"name": name, "namespace": ns}), state)
.await
}
ContextWorkspaceAction::List => {
ListContextWorkspacesHandler.execute(serde_json::json!({"namespace": ns}), state).await
ListContextWorkspacesHandler
.execute(serde_json::json!({"namespace": ns}), state)
.await
}
ContextWorkspaceAction::Delete => {
let name = req.name.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'name' for action 'delete'. Next step: Provide workspace 'name' string in request and retry.".to_string())
})?;
DeleteContextWorkspaceHandler.execute(serde_json::json!({"name": name, "namespace": ns}), state).await
DeleteContextWorkspaceHandler
.execute(serde_json::json!({"name": name, "namespace": ns}), state)
.await
}
ContextWorkspaceAction::Diff => {
let name = req.name.ok_or_else(|| {
@@ -593,19 +665,37 @@ impl McpTool for PrChecklistHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: PrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
let ns = req
.namespace
.unwrap_or_else(|| crate::models::default_namespace());
match req.action {
PrChecklistAction::Add => {
let desc = req.description.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'description' for action 'add'. Next step: Provide checklist item 'description' string in request and retry.".to_string())
})?;
AddPrChecklistItemHandler.execute(serde_json::json!({"description": desc, "namespace": ns}), state).await
AddPrChecklistItemHandler
.execute(
serde_json::json!({
"description": desc,
"namespace": ns,
"category": req.category,
"automated_check": req.automated_check,
"is_checked": req.is_checked,
"repo_name": req.repo_name,
}),
state,
)
.await
}
PrChecklistAction::Get => {
GetPrChecklistHandler.execute(serde_json::json!({"namespace": ns}), state).await
GetPrChecklistHandler
.execute(serde_json::json!({"namespace": ns}), state)
.await
}
PrChecklistAction::Clear => {
ClearPrChecklistHandler.execute(serde_json::json!({"namespace": ns}), state).await
ClearPrChecklistHandler
.execute(serde_json::json!({"namespace": ns}), state)
.await
}
}
}
@@ -636,37 +726,51 @@ impl McpTool for SnippetsHandler {
let lang = req.language.unwrap_or_else(|| "text".to_string());
let code = req.code.unwrap_or_default();
let desc = req.description.unwrap_or_default();
StoreSnippetHandler.execute(
serde_json::json!({
"name": name,
"language": lang,
"code": code,
"description": desc,
"tags": req.tags
}),
state,
).await
StoreSnippetHandler
.execute(
serde_json::json!({
"name": name,
"language": lang,
"code": code,
"description": desc,
"tags": req.tags,
"namespace": req.namespace,
"repo_name": req.repo_name,
"origin_file": req.origin_file,
"line_range": req.line_range,
}),
state,
)
.await
}
SnippetAction::Search => {
let q = req.query.or(req.name).unwrap_or_default();
if req.hybrid.unwrap_or(false) {
crate::handlers::meta::SearchSnippetsHybridHandler.execute(serde_json::json!({"query": q, "tags": req.tags}), state).await
crate::handlers::meta::SearchSnippetsHybridHandler
.execute(serde_json::json!({"query": q, "tags": req.tags}), state)
.await
} else {
SearchSnippetsHandler.execute(serde_json::json!({"query": q, "tags": req.tags}), state).await
SearchSnippetsHandler
.execute(serde_json::json!({"query": q, "tags": req.tags}), state)
.await
}
}
SnippetAction::Delete => {
let id = req.name.or(req.id).or(req.query).ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'name', 'id', or 'query' for action 'delete'. Next step: Provide snippet ID/name in request and retry.".to_string())
})?;
DeleteSnippetHandler.execute(serde_json::json!({"name": id}), state).await
DeleteSnippetHandler
.execute(serde_json::json!({"name": id}), state)
.await
}
SnippetAction::Tag => {
let id = req.name.or(req.id).or(req.query).ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'name', 'id', or 'query' for action 'tag'. Next step: Provide snippet ID/name and 'tags' array in request and retry.".to_string())
})?;
let tags = req.tags.unwrap_or_default();
TagSnippetHandler.execute(serde_json::json!({"name": id, "tags": tags}), state).await
TagSnippetHandler
.execute(serde_json::json!({"name": id, "tags": tags}), state)
.await
}
}
}
@@ -793,24 +897,42 @@ mod tests {
// Snapshots
let create_snap = CreateSnapshotHandler;
let snap_res = create_snap.execute(json!({"description": "Test snap", "namespace": "global"}), state.clone()).await.unwrap();
let snap_res = create_snap
.execute(
json!({"description": "Test snap", "namespace": "global"}),
state.clone(),
)
.await
.unwrap();
assert!(snap_res.contains("SNAP-"));
let snap_id = snap_res.split_whitespace().nth(1).unwrap();
let restore_snap = RestoreSnapshotHandler;
let restore_res = restore_snap.execute(json!({"snapshot_id": snap_id}), state.clone()).await.unwrap();
let restore_res = restore_snap
.execute(json!({"snapshot_id": snap_id}), state.clone())
.await
.unwrap();
assert!(restore_res.contains("Successfully restored"));
// Subagent Namespace
let create_sub_ns = CreateSubagentNamespaceHandler;
let sub_ns_res = create_sub_ns.execute(json!({"subagent_id": "test_agent"}), state.clone()).await.unwrap();
let sub_ns_res = create_sub_ns
.execute(json!({"subagent_id": "test_agent"}), state.clone())
.await
.unwrap();
assert!(sub_ns_res.contains("subagent-test_agent"));
let manage_sub_ns = ManageSubagentNamespaceHandler;
let manage_res = manage_sub_ns.execute(json!({
"subagent_id": "test_agent",
"action": "create"
}), state.clone()).await.unwrap();
let manage_res = manage_sub_ns
.execute(
json!({
"subagent_id": "test_agent",
"action": "create"
}),
state.clone(),
)
.await
.unwrap();
assert!(manage_res.contains("initialized"));
// Context Workspace Diff
@@ -818,9 +940,14 @@ mod tests {
save_ws.execute(json!({"name": "ws_a", "description": "Workspace A", "pinned_files": ["src/lib.rs"], "active_task_ids": []}), state.clone()).await.unwrap();
save_ws.execute(json!({"name": "ws_b", "description": "Workspace B", "pinned_files": ["src/main.rs"], "active_task_ids": []}), state.clone()).await.unwrap();
let diff_ws = DiffContextWorkspacesHandler;
let diff_res = diff_ws.execute(json!({"workspace_a": "ws_a", "workspace_b": "ws_b"}), state.clone()).await.unwrap();
let diff_res = diff_ws
.execute(
json!({"workspace_a": "ws_a", "workspace_b": "ws_b"}),
state.clone(),
)
.await
.unwrap();
assert!(diff_res.contains("workspace_a"));
}
@@ -834,7 +961,13 @@ mod tests {
save_ws.execute(json!({"name": "ws_2", "description": "Workspace 2", "pinned_files": ["src/main.rs"], "active_task_ids": []}), state.clone()).await.unwrap();
let diff_ws = DiffContextWorkspacesHandler;
let diff_res = diff_ws.execute(json!({"workspace_a": "ws_1", "workspace_b": "ws_2"}), state.clone()).await.unwrap();
let diff_res = diff_ws
.execute(
json!({"workspace_a": "ws_1", "workspace_b": "ws_2"}),
state.clone(),
)
.await
.unwrap();
assert!(diff_res.contains("ws_1"));
}
}
@@ -998,7 +1131,10 @@ impl McpTool for CreateSnapshotHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: CreateSnapshotTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let snapshot_id = format!("SNAP-{}", uuid::Uuid::new_v4().to_string()[..8].to_uppercase());
let snapshot_id = format!(
"SNAP-{}",
uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
);
let ns = req.namespace.unwrap_or_else(|| "global".to_string());
let snapshot = crate::models::StateSnapshot {
@@ -1006,14 +1142,22 @@ impl McpTool for CreateSnapshotHandler {
timestamp: crate::handlers::utils::now_secs(),
description: req.description.clone(),
namespace: ns.clone(),
..Default::default()
};
state.project.snapshots.modify(|snaps| {
snaps.push(snapshot);
});
state.record_activity("snapshot", &format!("Created snapshot {}: {}", snapshot_id, req.description), None);
Ok(format!("Snapshot {} created for namespace '{}'", snapshot_id, ns))
state.record_activity(
"snapshot",
&format!("Created snapshot {}: {}", snapshot_id, req.description),
None,
);
Ok(format!(
"Snapshot {} created for namespace '{}'",
snapshot_id, ns
))
}
}
@@ -1031,15 +1175,26 @@ impl McpTool for RestoreSnapshotHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: RestoreSnapshotTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let found = state.project.snapshots.read_with(|snaps| {
snaps.iter().any(|s| s.id == req.snapshot_id)
});
let found = state
.project
.snapshots
.read_with(|snaps| snaps.iter().any(|s| s.id == req.snapshot_id));
if found {
state.record_activity("snapshot", &format!("Restored snapshot {}", req.snapshot_id), None);
Ok(format!("Successfully restored memory state from snapshot {}", req.snapshot_id))
state.record_activity(
"snapshot",
&format!("Restored snapshot {}", req.snapshot_id),
None,
);
Ok(format!(
"Successfully restored memory state from snapshot {}",
req.snapshot_id
))
} else {
Err(crate::error::AppError::Internal(format!("Snapshot {} not found", req.snapshot_id)))
Err(crate::error::AppError::Internal(format!(
"Snapshot {} not found",
req.snapshot_id
)))
}
}
}
@@ -1060,20 +1215,35 @@ impl McpTool for ManageSubagentNamespaceHandler {
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ManageSubagentNamespaceTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let req: ManageSubagentNamespaceTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let subagent_ns = format!("subagent-{}", req.subagent_id);
match req.action {
SubagentNamespaceAction::Create => {
state.record_activity("namespace", &format!("Created subagent namespace {}", subagent_ns), None);
Ok(format!("Subagent memory namespace '{}' initialized.", subagent_ns))
state.record_activity(
"namespace",
&format!("Created subagent namespace {}", subagent_ns),
None,
);
Ok(format!(
"Subagent memory namespace '{}' initialized.",
subagent_ns
))
}
SubagentNamespaceAction::Purge => {
state.project.pinned_files.modify(|files| {
files.retain(|f| f.namespace != subagent_ns);
});
state.record_activity("namespace", &format!("Purged subagent namespace {}", subagent_ns), None);
Ok(format!("Subagent memory namespace '{}' purged.", subagent_ns))
state.record_activity(
"namespace",
&format!("Purged subagent namespace {}", subagent_ns),
None,
);
Ok(format!(
"Subagent memory namespace '{}' purged.",
subagent_ns
))
}
SubagentNamespaceAction::Condense => {
let purge = req.purge_after_promotion.unwrap_or(true);
@@ -1091,13 +1261,15 @@ impl McpTool for ManageSubagentNamespaceHandler {
for mut e in sub_entities {
promoted_entities += 1;
e.namespace = "global".to_string();
let global_entry = g.entities.entry(e.name.clone()).or_insert_with(|| Entity {
name: e.name.clone(),
entity_type: e.entity_type.clone(),
observations: Vec::new(),
namespace: "global".to_string(),
git_branch: e.git_branch.clone(),
});
let global_entry =
g.entities.entry(e.name.clone()).or_insert_with(|| Entity {
name: e.name.clone(),
entity_type: e.entity_type.clone(),
observations: Vec::new(),
namespace: "global".to_string(),
git_branch: e.git_branch.clone(),
..Default::default()
});
for obs in e.observations {
if !global_entry.observations.contains(&obs) {
global_entry.observations.push(obs);
@@ -1142,13 +1314,21 @@ impl McpTool for CreateSubagentNamespaceHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<CreateSubagentNamespaceTool>("create_subagent_namespace", "Create subagent namespace")
crate::mcp::tool_def::<CreateSubagentNamespaceTool>(
"create_subagent_namespace",
"Create subagent namespace",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: CreateSubagentNamespaceTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let req: CreateSubagentNamespaceTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = format!("subagent-{}", req.subagent_id);
state.record_activity("namespace", &format!("Created subagent namespace {}", ns), None);
state.record_activity(
"namespace",
&format!("Created subagent namespace {}", ns),
None,
);
Ok(format!("Subagent memory namespace '{}' initialized.", ns))
}
}
@@ -1162,18 +1342,26 @@ impl McpTool for PurgeSubagentNamespaceHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<PurgeSubagentNamespaceTool>("purge_subagent_namespace", "Purge subagent namespace")
crate::mcp::tool_def::<PurgeSubagentNamespaceTool>(
"purge_subagent_namespace",
"Purge subagent namespace",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: PurgeSubagentNamespaceTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let req: PurgeSubagentNamespaceTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = format!("subagent-{}", req.subagent_id);
state.project.pinned_files.modify(|files| {
files.retain(|f| f.namespace != ns);
});
state.record_activity("namespace", &format!("Purged subagent namespace {}", ns), None);
state.record_activity(
"namespace",
&format!("Purged subagent namespace {}", ns),
None,
);
Ok(format!("Subagent memory namespace '{}' purged.", ns))
}
}
@@ -1219,6 +1407,7 @@ impl McpTool for CondenseSubagentNamespaceHandler {
observations: Vec::new(),
namespace: "global".to_string(),
git_branch: e.git_branch.clone(),
..Default::default()
});
for obs in e.observations {
if !global_entry.observations.contains(&obs) {
@@ -1262,11 +1451,15 @@ impl McpTool for DiffContextWorkspacesHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<DiffContextWorkspacesTool>("diff_context_workspaces", "Execute diff_context_workspaces")
crate::mcp::tool_def::<DiffContextWorkspacesTool>(
"diff_context_workspaces",
"Execute diff_context_workspaces",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DiffContextWorkspacesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let req: DiffContextWorkspacesTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let workspaces = state.project.context_workspaces.read_with(|ws| ws.clone());
let ws_a = workspaces.iter().find(|w| w.name == req.workspace_a);
@@ -1275,7 +1468,10 @@ impl McpTool for DiffContextWorkspacesHandler {
if ws_a.is_none() || ws_b.is_none() {
return Err(crate::error::AppError::Internal(format!(
"One or both context workspaces not found: '{}' found={}, '{}' found={}",
req.workspace_a, ws_a.is_some(), req.workspace_b, ws_b.is_some()
req.workspace_a,
ws_a.is_some(),
req.workspace_b,
ws_b.is_some()
)));
}
@@ -1314,4 +1510,3 @@ impl McpTool for DiffContextWorkspacesHandler {
Ok(serde_json::to_string_pretty(&diff)?)
}
}