diff --git a/server/src/api/setup.rs b/server/src/api/setup.rs index c509813..4735e04 100644 --- a/server/src/api/setup.rs +++ b/server/src/api/setup.rs @@ -713,6 +713,42 @@ mod tests { assert!(js_str.contains("refreshActiveTab")); assert!(js_str.contains("parseActivityPayload")); } + + #[tokio::test] + async fn test_activity_feed_and_empty_state() { + let (_app, app_state, _dir) = setup_app().await; + + // 1. Initial GET /api/activity should return empty array [] + let app_act = create_router(app_state.clone()); + let req_act = Request::builder().uri("/api/activity").body(Body::empty()).unwrap(); + let resp_act = app_act.oneshot(req_act).await.unwrap(); + assert_eq!(resp_act.status(), StatusCode::OK); + let body_bytes = axum::body::to_bytes(resp_act.into_body(), usize::MAX).await.unwrap(); + let json_str = String::from_utf8(body_bytes.to_vec()).unwrap(); + assert_eq!(json_str.trim(), "[]"); + + // 2. Record an activity + app_state.handler.state.record_activity("code_change", "Refactored live activity UI", Some("Added empty state placeholder")); + + // 3. GET /api/activity should now return recorded item + let app_act2 = create_router(app_state.clone()); + let req_act2 = Request::builder().uri("/api/activity").body(Body::empty()).unwrap(); + let resp_act2 = app_act2.oneshot(req_act2).await.unwrap(); + assert_eq!(resp_act2.status(), StatusCode::OK); + let body_bytes2 = axum::body::to_bytes(resp_act2.into_body(), usize::MAX).await.unwrap(); + let json_str2 = String::from_utf8(body_bytes2.to_vec()).unwrap(); + assert!(json_str2.contains("CODE_CHANGE")); + assert!(json_str2.contains("Refactored live activity UI")); + + // 4. Verify /dashboard.js contains empty state text + let app_js = create_router(app_state.clone()); + let req_js = Request::builder().uri("/dashboard.js").body(Body::empty()).unwrap(); + let resp_js = app_js.oneshot(req_js).await.unwrap(); + assert_eq!(resp_js.status(), StatusCode::OK); + let js_bytes = axum::body::to_bytes(resp_js.into_body(), usize::MAX).await.unwrap(); + let js_str = String::from_utf8(js_bytes.to_vec()).unwrap(); + assert!(js_str.contains("No recent activity recorded yet.")); + } } diff --git a/server/src/dashboard.js b/server/src/dashboard.js index 2e3979c..b35d194 100644 --- a/server/src/dashboard.js +++ b/server/src/dashboard.js @@ -344,8 +344,40 @@ function parseActivityPayload(item) { return parseActivityPayload(item.params); } category = (item.category || item.type || "TOOL").toUpperCase(); - summary = item.summary || item.message || item.description || item.data || ""; - details = item.details || ""; + let rawSum = item.summary || item.message || item.description || item.data; + if (rawSum !== undefined && rawSum !== null) { + if (typeof rawSum === "object") { + summary = JSON.stringify(rawSum); + } else { + summary = String(rawSum); + } + } + if (!summary) { + if (item.title) + summary = String(item.title); + else if (item.name) + summary = String(item.name); + else if (item.action) + summary = String(item.action); + else if (item.content) + summary = String(item.content); + else { + const { timestamp: _t, time: _tm, updated_at: _u, category: _c, type: _tp, method: _m, params: _p, jsonrpc: _j, ...rest } = item; + if (Object.keys(rest).length > 0) { + summary = JSON.stringify(rest); + } else { + summary = JSON.stringify(item); + } + } + } + let rawDet = item.details; + if (rawDet !== undefined && rawDet !== null) { + if (typeof rawDet === "object") { + details = JSON.stringify(rawDet); + } else { + details = String(rawDet); + } + } timestamp = item.timestamp || item.time || item.updated_at || timestamp; if (typeof timestamp === "number" && timestamp < 10000000000) { timestamp = timestamp * 1000; @@ -356,15 +388,23 @@ function parseActivityPayload(item) { GRAPH: "#2ecc71", DECISION: "#f39c12", CODE: "#3498db", + CODE_CHANGE: "#3498db", TASK: "#1abc9c", + TASK_CREATE: "#1abc9c", + TASK_UPDATE: "#1abc9c", CLIPBOARD: "#9b59b6", STICKY_NOTE: "#e67e22", CHECKPOINT: "#e74c3c", ERROR_FIX: "#e74c3c", TECH_DEBT: "#d35400", SUBAGENT: "#8e44ad", + AGENT_SIGNAL: "#8e44ad", SNIPPET: "#16a085", TOOL: "#3498db", + EDITOR: "#9b59b6", + TERMINAL: "#27ae60", + SNAPSHOT: "#f1c40f", + NAMESPACE: "#e67e22", SYSTEM: "#95a5a6" }; const badgeColor = colorMap[catUpper] || "#3498db"; @@ -391,6 +431,10 @@ async function loadActivityHistory() { if (!feed) return; feed.innerHTML = ""; + if (!history || history.length === 0) { + feed.innerHTML = '
No recent activity recorded yet.
Live activity will stream here in real-time as MCP tools and system events execute.
'; + return; + } const getMillis = (item) => { if (!item) return 0; @@ -427,18 +471,23 @@ async function loadActivityHistory() { function setupWS() { const protocol = location.protocol === "https:" ? "wss:" : "ws:"; const ws = new WebSocket(`${protocol}//${location.host}/ws?client=ui`); - const feed = document.getElementById("activity-feed"); ws.onmessage = function(event) { try { const data = JSON.parse(event.data); if (data.type === "activity" || data.method === "notifications/activity" || data.method === "notifications/task/completed" || data.method === "notifications/resources/updated") { - refreshActiveTab(); - const isScrolledToTop = feed ? feed.scrollTop <= 20 : true; - const div = document.createElement("div"); - div.className = "feed-entry"; - const payload = data.params || data.data || data; - div.innerHTML = parseActivityPayload(payload); + if (currentTabId !== "activity-tab") { + refreshActiveTab(); + } + const feed = document.getElementById("activity-feed"); if (feed) { + if (feed.querySelector(".feed-entry") === null) { + feed.innerHTML = ""; + } + const isScrolledToTop = feed.scrollTop <= 20; + const div = document.createElement("div"); + div.className = "feed-entry"; + const payload = data.params || data.data || data; + div.innerHTML = parseActivityPayload(payload); feed.prepend(div); while (feed.children.length > MAX_ACTIVITY_HISTORY && feed.lastChild) { feed.removeChild(feed.lastChild); @@ -619,9 +668,14 @@ function setupSSE() { sse.onmessage = function(event) { if (event.data) { try { - refreshActiveTab(); + if (currentTabId !== "activity-tab") { + refreshActiveTab(); + } const feed = document.getElementById("activity-feed"); if (feed) { + if (feed.querySelector(".feed-entry") === null) { + feed.innerHTML = ""; + } const isScrolledToTop = feed.scrollTop <= 20; const div = document.createElement("div"); div.className = "feed-entry"; diff --git a/server/src/dashboard.ts b/server/src/dashboard.ts index f7610f4..0a1ee4e 100644 --- a/server/src/dashboard.ts +++ b/server/src/dashboard.ts @@ -538,8 +538,39 @@ function parseActivityPayload(item: any): string { return parseActivityPayload(item.params); } category = (item.category || item.type || 'TOOL').toUpperCase(); - summary = item.summary || item.message || item.description || item.data || ''; - details = item.details || ''; + + let rawSum = item.summary || item.message || item.description || item.data; + if (rawSum !== undefined && rawSum !== null) { + if (typeof rawSum === 'object') { + summary = JSON.stringify(rawSum); + } else { + summary = String(rawSum); + } + } + if (!summary) { + if (item.title) summary = String(item.title); + else if (item.name) summary = String(item.name); + else if (item.action) summary = String(item.action); + else if (item.content) summary = String(item.content); + else { + const { timestamp: _t, time: _tm, updated_at: _u, category: _c, type: _tp, method: _m, params: _p, jsonrpc: _j, ...rest } = item; + if (Object.keys(rest).length > 0) { + summary = JSON.stringify(rest); + } else { + summary = JSON.stringify(item); + } + } + } + + let rawDet = item.details; + if (rawDet !== undefined && rawDet !== null) { + if (typeof rawDet === 'object') { + details = JSON.stringify(rawDet); + } else { + details = String(rawDet); + } + } + timestamp = item.timestamp || item.time || item.updated_at || timestamp; if (typeof timestamp === 'number' && timestamp < 10000000000) { @@ -552,15 +583,23 @@ function parseActivityPayload(item: any): string { 'GRAPH': '#2ecc71', 'DECISION': '#f39c12', 'CODE': '#3498db', + 'CODE_CHANGE': '#3498db', 'TASK': '#1abc9c', + 'TASK_CREATE': '#1abc9c', + 'TASK_UPDATE': '#1abc9c', 'CLIPBOARD': '#9b59b6', 'STICKY_NOTE': '#e67e22', 'CHECKPOINT': '#e74c3c', 'ERROR_FIX': '#e74c3c', 'TECH_DEBT': '#d35400', 'SUBAGENT': '#8e44ad', + 'AGENT_SIGNAL': '#8e44ad', 'SNIPPET': '#16a085', 'TOOL': '#3498db', + 'EDITOR': '#9b59b6', + 'TERMINAL': '#27ae60', + 'SNAPSHOT': '#f1c40f', + 'NAMESPACE': '#e67e22', 'SYSTEM': '#95a5a6' }; const badgeColor = colorMap[catUpper] || '#3498db'; @@ -586,6 +625,11 @@ async function loadActivityHistory(): Promise { if (!feed) return; feed.innerHTML = ''; + if (!history || history.length === 0) { + feed.innerHTML = '
No recent activity recorded yet.
Live activity will stream here in real-time as MCP tools and system events execute.
'; + return; + } + const getMillis = (item: any): number => { if (!item) return 0; if (typeof item === 'string') { @@ -620,20 +664,24 @@ async function loadActivityHistory(): Promise { function setupWS(): void { const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${protocol}//${location.host}/ws?client=ui`); - const feed = document.getElementById('activity-feed'); ws.onmessage = function(event: MessageEvent) { try { const data = JSON.parse(event.data); if (data.type === 'activity' || data.method === 'notifications/activity' || data.method === 'notifications/task/completed' || data.method === 'notifications/resources/updated') { - refreshActiveTab(); - const isScrolledToTop = feed ? feed.scrollTop <= 20 : true; - - const div = document.createElement('div'); - div.className = 'feed-entry'; - const payload = data.params || data.data || data; - div.innerHTML = parseActivityPayload(payload); + if (currentTabId !== 'activity-tab') { + refreshActiveTab(); + } + const feed = document.getElementById('activity-feed'); if (feed) { + if (feed.querySelector('.feed-entry') === null) { + feed.innerHTML = ''; + } + const isScrolledToTop = feed.scrollTop <= 20; + const div = document.createElement('div'); + div.className = 'feed-entry'; + const payload = data.params || data.data || data; + div.innerHTML = parseActivityPayload(payload); feed.prepend(div); while (feed.children.length > MAX_ACTIVITY_HISTORY && feed.lastChild) { feed.removeChild(feed.lastChild); @@ -871,9 +919,14 @@ function setupSSE(): void { sse.onmessage = function(event: MessageEvent) { if (event.data) { try { - refreshActiveTab(); + if (currentTabId !== 'activity-tab') { + refreshActiveTab(); + } const feed = document.getElementById('activity-feed'); if (feed) { + if (feed.querySelector('.feed-entry') === null) { + feed.innerHTML = ''; + } const isScrolledToTop = feed.scrollTop <= 20; const div = document.createElement('div'); div.className = 'feed-entry';