From 6e0ccf8082bc4866413a10ab65f8d10ad528ffcf Mon Sep 17 00:00:00 2001 From: Riz Ashraf Date: Mon, 5 Oct 2026 10:48:43 +0100 Subject: [PATCH] refactor(justfile): drop aliases/duplication, enforce single canonical commands, and add descriptions for all recipes --- server/src/dashboard.html | 56 ++++++++++------------- server/src/router.rs | 96 +++++++++++++++++++++------------------ server/src/state.rs | 90 ++++++++++++++++++++++++------------ 3 files changed, 135 insertions(+), 107 deletions(-) diff --git a/server/src/dashboard.html b/server/src/dashboard.html index 18f3f7c..9e93589 100644 --- a/server/src/dashboard.html +++ b/server/src/dashboard.html @@ -1061,8 +1061,9 @@ const MAX_ACTIVITY_HISTORY = 100; function parseActivityPayload(item) { - let category = 'activity'; - let message = ''; + let category = 'TOOL'; + let summary = ''; + let details = ''; let timestamp = Date.now(); if (typeof item === 'string') { @@ -1070,40 +1071,19 @@ const parsed = JSON.parse(item); return parseActivityPayload(parsed); } catch (e) { - message = item; + summary = item; } } else if (typeof item === 'object' && item !== null) { if (item.method === 'notifications/activity' && item.params) { - message = item.params.message || JSON.stringify(item.params); - timestamp = item.params.time || item.params.timestamp || timestamp; - category = item.params.category || item.params.type || 'tool'; - } else { - category = item.category || item.type || category; - message = item.summary || item.message || item.description || item.data || JSON.stringify(item); - timestamp = item.timestamp || item.time || item.updated_at || timestamp; - if (typeof timestamp === 'number' && timestamp < 10000000000) { - timestamp = timestamp * 1000; - } + return parseActivityPayload(item.params); } - } + category = (item.category || item.type || 'TOOL').toUpperCase(); + summary = item.summary || item.message || item.description || item.data || ''; + details = item.details || ''; + timestamp = item.timestamp || item.time || item.updated_at || timestamp; - // Fallback inference from tool name if category is generic - const lowerCategory = category.toLowerCase(); - if (lowerCategory === 'activity' || lowerCategory === 'mcp_tool' || lowerCategory === 'tool') { - const match = message.match(/Agent executed tool:\s*([a-z0-9_]+)/i); - if (match && match[1]) { - const tool = match[1].toLowerCase(); - if (tool.includes('clipboard')) category = 'CLIPBOARD'; - else if (tool.includes('graph') || tool.includes('entities') || tool.includes('relations') || tool.includes('schema')) category = 'GRAPH'; - else if (tool.includes('decision')) category = 'DECISION'; - else if (tool.includes('code_change') || tool.includes('ast')) category = 'CODE'; - else if (tool.includes('task') || tool.includes('milestone')) category = 'TASK'; - else if (tool.includes('note')) category = 'STICKY_NOTE'; - else if (tool.includes('checkpoint') || tool.includes('snapshot')) category = 'CHECKPOINT'; - else if (tool.includes('subagent') || tool.includes('namespace')) category = 'SUBAGENT'; - else if (tool.includes('snippet')) category = 'SNIPPET'; - else if (tool.includes('error')) category = 'ERROR_FIX'; - else category = tool.toUpperCase(); + if (typeof timestamp === 'number' && timestamp < 10000000000) { + timestamp = timestamp * 1000; } } @@ -1131,7 +1111,11 @@ month: '2-digit', day: '2-digit', hour: '2-digit', minute:'2-digit', second:'2-digit' }); - return `[${timeStr}] ${catUpper} ${message}`; + let html = `[${timeStr}] ${escapeHtml(catUpper)} ${escapeHtml(summary)}`; + if (details) { + html += `
${escapeHtml(details)}
`; + } + return html; } async function loadActivityHistory() { @@ -1144,7 +1128,13 @@ // Helper to normalize timestamp to epoch milliseconds const getMillis = (item) => { - if (!item || typeof item !== 'object') return 0; + if (!item) return 0; + if (typeof item === 'string') { + try { item = JSON.parse(item); } catch(e) { return 0; } + } + if (item.method === 'notifications/activity' && item.params) { + item = item.params; + } let val = item.timestamp || item.time || item.updated_at || 0; if (typeof val === 'number' && val < 10000000000) { val = val * 1000; diff --git a/server/src/router.rs b/server/src/router.rs index ce9c7b3..d966544 100644 --- a/server/src/router.rs +++ b/server/src/router.rs @@ -820,7 +820,7 @@ impl MemoryHandler { } pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) -> String { - let detail = match name { + let (action, detail) = match name { "log_code_change" => { let file = args.get("file_path") .or_else(|| args.get("file")) @@ -831,40 +831,44 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) -> .or_else(|| args.get("description")) .and_then(|v| v.as_str()); - match (file, summary) { + let d = match (file, summary) { (Some(f), Some(s)) => format!("{}: {}", f, s), (Some(f), None) => f.to_string(), (None, Some(s)) => s.to_string(), - (None, None) => "".to_string(), - } + (None, None) => String::new(), + }; + ("Log Code Change", d) } "log_decision" => { - args.get("title") + let d = args.get("title") .or_else(|| args.get("decision")) .or_else(|| args.get("summary")) .and_then(|v| v.as_str()) .unwrap_or("") - .to_string() + .to_string(); + ("Log Decision", d) } "log_error_fix" => { - args.get("error") + let d = args.get("error") .or_else(|| args.get("summary")) .or_else(|| args.get("solution")) .and_then(|v| v.as_str()) .unwrap_or("") - .to_string() + .to_string(); + ("Log Error Fix", d) } "log_tech_debt" => { - if let Some(summary) = args.get("summary").or_else(|| args.get("description")).and_then(|v| v.as_str()) { + let d = if let Some(summary) = args.get("summary").or_else(|| args.get("description")).and_then(|v| v.as_str()) { summary.to_string() } else if let Some(file) = args.get("file_path").or_else(|| args.get("file")).and_then(|v| v.as_str()) { file.to_string() } else { - "".to_string() - } + String::new() + }; + ("Log Tech Debt", d) } "create_entities" => { - if let Some(entities) = args.get("entities").and_then(|v| v.as_array()) { + let d = if let Some(entities) = args.get("entities").and_then(|v| v.as_array()) { let names: Vec<&str> = entities .iter() .filter_map(|e| e.get("name").or_else(|| e.get("entity_name")).and_then(|n| n.as_str())) @@ -882,11 +886,12 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) -> } else if let Some(name) = args.get("name").or_else(|| args.get("entity_name")).and_then(|v| v.as_str()) { name.to_string() } else { - "".to_string() - } + String::new() + }; + ("Create Entities", d) } "create_relations" => { - if let Some(relations) = args.get("relations").and_then(|v| v.as_array()) { + let d = if let Some(relations) = args.get("relations").and_then(|v| v.as_array()) { if let Some(first) = relations.first() { let from = first.get("from").and_then(|v| v.as_str()).unwrap_or("?"); let to = first.get("to").and_then(|v| v.as_str()).unwrap_or("?"); @@ -900,55 +905,57 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) -> format!("{} relation(s)", relations.len()) } } else { - "".to_string() - } + String::new() + }; + ("Create Relations", d) } "add_task" => { - args.get("title") + let d = args.get("title") .or_else(|| args.get("name")) .and_then(|v| v.as_str()) .unwrap_or("") - .to_string() + .to_string(); + ("Add Task", d) } "update_task_status" => { - if let (Some(id), Some(status)) = ( + let d = if let (Some(id), Some(status)) = ( args.get("task_id").or_else(|| args.get("id")).and_then(|v| v.as_str()), args.get("status").and_then(|v| v.as_str()), ) { format!("Task {} -> {}", id, status) } else { - "".to_string() - } + String::new() + }; + ("Update Task Status", d) } "omni_search" | "search_graph" | "search_snippets" | "search_web" => { - args.get("query") + let d = args.get("query") .and_then(|v| v.as_str()) .map(|q| format!("\"{}\"", q)) - .unwrap_or_default() + .unwrap_or_default(); + ("Search", d) } - "manage_sticky_notes" => { - let action = args.get("action").and_then(|v| v.as_str()).unwrap_or(""); - if let Some(content) = args.get("content").and_then(|v| v.as_str()) { + "manage_sticky_notes" | "add_sticky_note" => { + let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("add"); + let d = if let Some(content) = args.get("content").and_then(|v| v.as_str()) { let preview: String = content.chars().take(40).collect(); - if !action.is_empty() { - format!("{} \"{}\"", action, preview) - } else { - format!("\"{}\"", preview) - } + format!("{} \"{}\"", action, preview) } else { action.to_string() - } + }; + ("Sticky Note", d) } "write_clipboard" => { - if let Some(text) = args.get("text").or_else(|| args.get("content")).and_then(|v| v.as_str()) { + let d = if let Some(text) = args.get("text").or_else(|| args.get("content")).and_then(|v| v.as_str()) { let preview: String = text.chars().take(40).collect(); format!("\"{}\"", preview) } else { - "".to_string() - } + String::new() + }; + ("Write Clipboard", d) } _ => { - if let Some(title) = args + let d = if let Some(title) = args .get("title") .or_else(|| args.get("summary")) .or_else(|| args.get("description")) @@ -961,8 +968,9 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) -> { title.to_string() } else { - "".to_string() - } + String::new() + }; + (name, d) } }; @@ -973,9 +981,9 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) -> } if !truncated.is_empty() { - format!("Agent executed tool: {} ({})", name, truncated) + format!("{}: {}", action, truncated) } else { - format!("Agent executed tool: {}", name) + format!("{}", action) } } @@ -988,13 +996,13 @@ mod tests { #[test] fn test_format_tool_activity_description() { let msg1 = format_tool_activity_description("log_code_change", &json!({ "file": "src/lib.rs", "summary": "Refactored library" })); - assert_eq!(msg1, "Agent executed tool: log_code_change (src/lib.rs: Refactored library)"); + assert_eq!(msg1, "Log Code Change: src/lib.rs: Refactored library"); let msg2 = format_tool_activity_description("create_entities", &json!({ "entities": [{ "name": "EntityA" }, { "name": "EntityB" }] })); - assert_eq!(msg2, "Agent executed tool: create_entities (EntityA, EntityB)"); + assert_eq!(msg2, "Create Entities: EntityA, EntityB"); let msg3 = format_tool_activity_description("read_graph", &json!({})); - assert_eq!(msg3, "Agent executed tool: read_graph"); + assert_eq!(msg3, "read_graph"); } #[tokio::test] diff --git a/server/src/state.rs b/server/src/state.rs index 4b13180..8f832c8 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -140,31 +140,7 @@ impl MemoryState { } pub fn broadcast_activity(&self, category: &str, message: &str) { - let time = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64; - - let item = serde_json::json!({ - "time": time, - "category": category, - "message": message - }); - - self.telemetry.recent_activities.modify(|activities| { - activities.push_front(item.clone()); - if activities.len() > 100 { - activities.pop_back(); - } - }); - - let payload = serde_json::json!({ - "jsonrpc": "2.0", - "method": "notifications/activity", - "params": item - }) - .to_string(); - let _ = self.activity_tx.send(payload); + self.record_activity(category, message, None); } pub fn read_graph(&self, f: F) -> R @@ -237,25 +213,34 @@ impl MemoryState { let ts = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() - .as_secs(); + .as_millis() as u64; + + let category_upper = category.to_uppercase(); let activity = ActivityRecord { timestamp: ts, - category: category.to_string(), + category: category_upper, summary: summary.to_string(), details: details.map(|s| s.to_string()), }; - let record = serde_json::to_value(&activity).unwrap_or_default(); + let record_val = serde_json::to_value(&activity).unwrap_or_default(); self.telemetry.recent_activities.modify(|activities| { - activities.push_front(record.clone()); + activities.push_front(record_val.clone()); if activities.len() > 100 { activities.pop_back(); } }); - let _ = self.activity_tx.send(record.to_string()); + let payload = serde_json::json!({ + "jsonrpc": "2.0", + "method": "notifications/activity", + "params": activity + }) + .to_string(); + + let _ = self.activity_tx.send(payload); } pub fn record_terminal_history(&self, payload: TerminalHistory) { @@ -329,6 +314,51 @@ mod tests { ); assert_eq!(results[0].1, "task", "Expected document type to be task"); } + + #[tokio::test] + async fn test_record_and_broadcast_activity() { + let dir = tempdir().unwrap(); + let state = MemoryState::new(dir.path().to_str().unwrap()); + let mut rx = state.activity_tx.subscribe(); + + // 1. Record an activity with details + state.record_activity("code_change", "Refactored state.rs", Some("Updated ActivityRecord schema")); + + // Verify recent_activities store + let activities: Vec = state.telemetry.recent_activities.read_with(|act| { + act.iter() + .filter_map(|v| serde_json::from_value(v.clone()).ok()) + .collect() + }); + + assert_eq!(activities.len(), 1); + assert_eq!(activities[0].category, "CODE_CHANGE"); + assert_eq!(activities[0].summary, "Refactored state.rs"); + assert_eq!(activities[0].details, Some("Updated ActivityRecord schema".to_string())); + assert!(activities[0].timestamp > 1_700_000_000_000, "Timestamp must be in epoch milliseconds"); + + // Verify broadcast channel message + let broadcast_msg = rx.recv().await.expect("Expected broadcast notification"); + let broadcast_val: serde_json::Value = serde_json::from_str(&broadcast_msg).expect("Valid JSON"); + assert_eq!(broadcast_val["jsonrpc"], "2.0"); + assert_eq!(broadcast_val["method"], "notifications/activity"); + assert_eq!(broadcast_val["params"]["category"], "CODE_CHANGE"); + + // 2. Broadcast an activity without details + state.broadcast_activity("task", "Completed live activity fix"); + + let activities_updated: Vec = state.telemetry.recent_activities.read_with(|act| { + act.iter() + .filter_map(|v| serde_json::from_value(v.clone()).ok()) + .collect() + }); + + assert_eq!(activities_updated.len(), 2); + assert_eq!(activities_updated[0].category, "TASK"); + assert_eq!(activities_updated[0].summary, "Completed live activity fix"); + assert_eq!(activities_updated[0].details, None); + assert!(activities_updated[0].timestamp >= activities_updated[1].timestamp); + } } use crate::embedding::{cosine_similarity, generate_embedding_async, generate_embeddings_async};