feat(embedding,vision): candle embeddings with offline fallback, on-demand clipboard vision capture, and concurrency audit
This commit is contained in:
1 parent
5bd8b1587a
commit
e4a0fe72df
47 files changed
+6292
-3503
No files matched your search
+120
-33
@@ -45,21 +45,34 @@ impl McpTool for TasksHandler {
|
||||
dependencies: deps,
|
||||
acceptance_criteria: vec![],
|
||||
expires_at: None,
|
||||
namespace: req.namespace.unwrap_or_else(|| "default".to_string()),
|
||||
repo_name: req.repo_name,
|
||||
priority: req.priority,
|
||||
assigned_agent: req.assigned_agent,
|
||||
verification_command: req.verification_command,
|
||||
..Default::default()
|
||||
};
|
||||
let idx = state.get_search_index().await;
|
||||
drop(idx.index_task(&task));
|
||||
state.project.tasks.modify(|tasks| {
|
||||
tasks.push(task.clone());
|
||||
});
|
||||
state.record_activity("task_create", &format!("Created task: {}", task.title), Some(&task.description));
|
||||
state.record_activity(
|
||||
"task_create",
|
||||
&format!("Created task: {}", task.title),
|
||||
Some(&task.description),
|
||||
);
|
||||
state.broadcast_task_event(TaskEvent {
|
||||
task_id: task_id.clone(),
|
||||
status: "created".to_string(),
|
||||
action: Some("add".to_string()),
|
||||
result: Some(serde_json::json!({ "title": task.title, "git_branch": task.git_branch })),
|
||||
result: Some(
|
||||
serde_json::json!({ "title": task.title, "git_branch": task.git_branch }),
|
||||
),
|
||||
error: None,
|
||||
timestamp: now,
|
||||
session_id: None,
|
||||
..Default::default()
|
||||
});
|
||||
Ok(format!("Task added with ID: {}", task_id))
|
||||
}
|
||||
@@ -75,6 +88,8 @@ impl McpTool for TasksHandler {
|
||||
let mut blocked = false;
|
||||
let mut blocker_details = String::new();
|
||||
|
||||
let mut updated_task: Option<Task> = None;
|
||||
|
||||
state.project.tasks.modify(|tasks| {
|
||||
let target_idx = tasks.iter().position(|t| t.id == id || t.title == id);
|
||||
let target_idx = match target_idx {
|
||||
@@ -84,7 +99,11 @@ impl McpTool for TasksHandler {
|
||||
found = true;
|
||||
|
||||
if target_status == "done" || target_status == "completed" {
|
||||
if tasks[target_idx].acceptance_criteria.iter().any(|c| !c.is_met) {
|
||||
if tasks[target_idx]
|
||||
.acceptance_criteria
|
||||
.iter()
|
||||
.any(|c| !c.is_met)
|
||||
{
|
||||
blocked = true;
|
||||
blocker_details = "Unmet acceptance criteria exist.".to_string();
|
||||
}
|
||||
@@ -100,20 +119,29 @@ impl McpTool for TasksHandler {
|
||||
}
|
||||
if !uncompleted_deps.is_empty() {
|
||||
blocked = true;
|
||||
blocker_details = format!("Blocked by dependencies: {}", uncompleted_deps.join(", "));
|
||||
blocker_details = format!(
|
||||
"Blocked by dependencies: {}",
|
||||
uncompleted_deps.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
if !blocked {
|
||||
let target_id_ref = tasks[target_idx].id.as_str();
|
||||
let mut uncompleted_children = Vec::new();
|
||||
for child in tasks.iter().filter(|t| t.parent_id.as_deref() == Some(target_id_ref)) {
|
||||
for child in tasks
|
||||
.iter()
|
||||
.filter(|t| t.parent_id.as_deref() == Some(target_id_ref))
|
||||
{
|
||||
if child.status != "completed" && child.status != "done" {
|
||||
uncompleted_children.push(child.title.as_str());
|
||||
}
|
||||
}
|
||||
if !uncompleted_children.is_empty() {
|
||||
blocked = true;
|
||||
blocker_details = format!("Blocked by child tasks: {}", uncompleted_children.join(", "));
|
||||
blocker_details = format!(
|
||||
"Blocked by child tasks: {}",
|
||||
uncompleted_children.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +149,7 @@ impl McpTool for TasksHandler {
|
||||
if !blocked {
|
||||
tasks[target_idx].status = target_status.clone();
|
||||
tasks[target_idx].updated_at = crate::handlers::utils::now_secs();
|
||||
updated_task = Some(tasks[target_idx].clone());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,10 +162,22 @@ impl McpTool for TasksHandler {
|
||||
error: Some(blocker_details.clone()),
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
session_id: None,
|
||||
..Default::default()
|
||||
});
|
||||
Err(crate::error::AppError::Internal(format!("Error: Cannot transition task. {}", blocker_details)))
|
||||
Err(crate::error::AppError::Internal(format!(
|
||||
"Error: Cannot transition task. {}",
|
||||
blocker_details
|
||||
)))
|
||||
} else if found {
|
||||
state.record_activity("task_update", &format!("Task {} status -> {}", id, status), None);
|
||||
if let Some(ref ut) = updated_task {
|
||||
let idx = state.get_search_index().await;
|
||||
drop(idx.index_task(ut));
|
||||
}
|
||||
state.record_activity(
|
||||
"task_update",
|
||||
&format!("Task {} status -> {}", id, status),
|
||||
None,
|
||||
);
|
||||
state.broadcast_task_event(TaskEvent {
|
||||
task_id: id.clone(),
|
||||
status: target_status.clone(),
|
||||
@@ -145,10 +186,13 @@ impl McpTool for TasksHandler {
|
||||
error: None,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
session_id: None,
|
||||
..Default::default()
|
||||
});
|
||||
Ok("Task status updated.".to_string())
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal("Task not found. Please verify the task ID.".to_string()))
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Task not found. Please verify the task ID.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
TaskAction::Delete => {
|
||||
@@ -163,9 +207,12 @@ impl McpTool for TasksHandler {
|
||||
for (idx, t) in tasks.iter().enumerate() {
|
||||
id_to_index.insert(t.id.as_str(), idx);
|
||||
}
|
||||
let mut children_map: std::collections::HashMap<usize, Vec<usize>> = std::collections::HashMap::new();
|
||||
let mut children_map: std::collections::HashMap<usize, Vec<usize>> =
|
||||
std::collections::HashMap::new();
|
||||
for (idx, t) in tasks.iter().enumerate() {
|
||||
if let Some(pid) = &t.parent_id && let Some(&p_idx) = id_to_index.get(pid.as_str()) {
|
||||
if let Some(pid) = &t.parent_id
|
||||
&& let Some(&p_idx) = id_to_index.get(pid.as_str())
|
||||
{
|
||||
children_map.entry(p_idx).or_default().push(idx);
|
||||
}
|
||||
}
|
||||
@@ -174,7 +221,9 @@ impl McpTool for TasksHandler {
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
queue.push_back(start_idx);
|
||||
while let Some(curr) = queue.pop_front() {
|
||||
if to_delete_idx.insert(curr) && let Some(children) = children_map.get(&curr) {
|
||||
if to_delete_idx.insert(curr)
|
||||
&& let Some(children) = children_map.get(&curr)
|
||||
{
|
||||
queue.extend(children.iter().copied());
|
||||
}
|
||||
}
|
||||
@@ -199,19 +248,29 @@ impl McpTool for TasksHandler {
|
||||
error: None,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
session_id: None,
|
||||
..Default::default()
|
||||
});
|
||||
Ok(format!("Deleted task and its children ({} total).", deleted_count))
|
||||
Ok(format!(
|
||||
"Deleted task and its children ({} total).",
|
||||
deleted_count
|
||||
))
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal("Task not found. Please verify the task ID.".to_string()))
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Task not found. Please verify the task ID.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
TaskAction::List => {
|
||||
let level = req.summary_level.as_deref().unwrap_or("detailed");
|
||||
let data = state.project.tasks.read_with(|tasks| {
|
||||
let filtered: Vec<_> = tasks
|
||||
let mut filtered: Vec<_> = tasks
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
let status_match = t.status != "done" && t.status != "completed";
|
||||
let status_match = match req.status.as_deref() {
|
||||
Some("all") => true,
|
||||
Some(s) => t.status.eq_ignore_ascii_case(s),
|
||||
None => t.status != "done" && t.status != "completed",
|
||||
};
|
||||
let branch_match = match &req.git_branch {
|
||||
Some(branch) => t.git_branch.is_none() || t.git_branch.as_deref() == Some(branch.as_str()),
|
||||
None => true,
|
||||
@@ -227,10 +286,9 @@ impl McpTool for TasksHandler {
|
||||
let mut json_str = serde_json::to_string(&filtered)?;
|
||||
if let Some(max_t) = req.max_tokens {
|
||||
let char_limit = max_t * 4;
|
||||
if json_str.len() > char_limit {
|
||||
let boundary = json_str.floor_char_boundary(char_limit);
|
||||
json_str.truncate(boundary);
|
||||
json_str.push_str(" ...[truncated due to max_tokens]");
|
||||
while json_str.len() > char_limit && !filtered.is_empty() {
|
||||
filtered.pop();
|
||||
json_str = serde_json::to_string(&filtered)?;
|
||||
}
|
||||
}
|
||||
Ok::<String, crate::error::AppError>(json_str)
|
||||
@@ -253,6 +311,7 @@ impl McpTool for TasksHandler {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
description: desc,
|
||||
is_met: false,
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
task.updated_at = crate::handlers::utils::now_secs();
|
||||
@@ -268,10 +327,13 @@ impl McpTool for TasksHandler {
|
||||
error: None,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
session_id: None,
|
||||
..Default::default()
|
||||
});
|
||||
Ok("Acceptance criteria set successfully.".to_string())
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal("Task not found. Please verify the task ID.".to_string()))
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Task not found. Please verify the task ID.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
TaskAction::Verify => {
|
||||
@@ -292,11 +354,13 @@ impl McpTool for TasksHandler {
|
||||
task.updated_at = crate::handlers::utils::now_secs();
|
||||
}
|
||||
} else {
|
||||
task.acceptance_criteria.push(crate::models::AcceptanceCriteria {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
description: proof_str.clone(),
|
||||
is_met: true,
|
||||
});
|
||||
task.acceptance_criteria
|
||||
.push(crate::models::AcceptanceCriteria {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
description: proof_str.clone(),
|
||||
is_met: true,
|
||||
..Default::default()
|
||||
});
|
||||
task.updated_at = crate::handlers::utils::now_secs();
|
||||
success = true;
|
||||
}
|
||||
@@ -311,12 +375,18 @@ impl McpTool for TasksHandler {
|
||||
error: None,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
session_id: None,
|
||||
..Default::default()
|
||||
});
|
||||
Ok(format!("Acceptance criteria verified with proof: {}", proof_str))
|
||||
Ok(format!(
|
||||
"Acceptance criteria verified with proof: {}",
|
||||
proof_str
|
||||
))
|
||||
} else if already_met {
|
||||
Ok("Acceptance criteria was already met.".to_string())
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal("Task not found. Please verify the task ID.".to_string()))
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Task not found. Please verify the task ID.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,14 +415,23 @@ impl McpTool for MilestonesHandler {
|
||||
let title = req.title.ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string())
|
||||
})?;
|
||||
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
|
||||
let ns = req
|
||||
.namespace
|
||||
.unwrap_or_else(|| crate::models::default_namespace());
|
||||
state.project.milestones.modify(|ms| {
|
||||
ms.push(crate::models::Milestone {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
title,
|
||||
status: "pending".to_string(),
|
||||
namespace: ns,
|
||||
target_date: None,
|
||||
target_date: req
|
||||
.target_date
|
||||
.as_deref()
|
||||
.and_then(|s| s.parse::<u64>().ok()),
|
||||
description: req.description,
|
||||
deliverables: req.deliverables.unwrap_or_default(),
|
||||
repo_name: req.repo_name,
|
||||
..Default::default()
|
||||
})
|
||||
});
|
||||
Ok("Milestone added".to_string())
|
||||
@@ -377,7 +456,9 @@ impl McpTool for MilestonesHandler {
|
||||
if found {
|
||||
Ok("Milestone updated".to_string())
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal("Milestone not found. Please verify the milestone ID.".to_string()))
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Milestone not found. Please verify the milestone ID.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
MilestoneAction::List => {
|
||||
@@ -504,7 +585,10 @@ mod tests {
|
||||
assert_eq!(res2, "Milestone updated");
|
||||
|
||||
let res3 = handler_ms
|
||||
.execute(json!({"action": "list", "namespace": "global"}), state.clone())
|
||||
.execute(
|
||||
json!({"action": "list", "namespace": "global"}),
|
||||
state.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
@@ -602,7 +686,10 @@ mod tests {
|
||||
assert!(res1.contains("Milestone added"));
|
||||
|
||||
let res2 = handler
|
||||
.execute(serde_json::json!({"action": "list", "namespace": "global"}), state.clone())
|
||||
.execute(
|
||||
serde_json::json!({"action": "list", "namespace": "global"}),
|
||||
state.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
|
||||
Reference in new issue
Block a user