diff --git a/backend/Controllers/DashboardController.cs b/backend/Controllers/DashboardController.cs index 38b0085..418fefe 100644 --- a/backend/Controllers/DashboardController.cs +++ b/backend/Controllers/DashboardController.cs @@ -343,6 +343,23 @@ public class DashboardController( } } + /// + /// Returns the most recent activity entries (assistant messages) for a specific agent. + /// + [HttpGet("agents/{id}/activity")] + public async Task> GetAgentActivity(string id, [FromQuery] int limit = 5) + { + try + { + return await gateway.GetAgentActivityAsync(id, Math.Clamp(limit, 1, 20)); + } + catch (Exception ex) + { + logger.LogWarning(ex, "GetAgentActivity failed for {AgentId}", id); + return new List(); + } + } + /// /// Returns the list of available models that can be assigned to agents. /// Reads from OpenClaw config dynamically, falls back to hardcoded list. diff --git a/backend/Models/Dashboard.cs b/backend/Models/Dashboard.cs index 09f7c69..863ff4a 100644 --- a/backend/Models/Dashboard.cs +++ b/backend/Models/Dashboard.cs @@ -104,3 +104,8 @@ public sealed record UpdateDashboardTaskRequest( public sealed record UpdateDashboardTaskStatusRequest( string Status ); + +public sealed record AgentActivityEntry( + string Time, + string Text +); diff --git a/backend/Services/OpenClawGatewayClient.cs b/backend/Services/OpenClawGatewayClient.cs index cc02760..d3e7a25 100644 --- a/backend/Services/OpenClawGatewayClient.cs +++ b/backend/Services/OpenClawGatewayClient.cs @@ -953,6 +953,44 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration return Math.Clamp(totalQueuePressure + agentPressure, 0, 100); } + /// + /// Fetches the most recent assistant activity (last N messages) for a specific agent. + /// Returns entries with timestamp and truncated content text. + /// Falls back to an empty list if the session is unreachable. + /// + public async Task> GetAgentActivityAsync(string agentId, int limit = 5) + { + var entries = new List(); + try + { + var sessionKey = $"agent:{agentId}:main"; + var messages = await GetSessionHistoryAsync(sessionKey, Math.Clamp(limit * 2, 1, 100)); + foreach (var msg in messages) + { + if (!string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase)) + continue; + if (string.IsNullOrWhiteSpace(msg.Content)) + continue; + if (msg.Content == "REPLY_SKIP" || msg.Content == "ANNOUNCE_SKIP") + continue; + + // Truncate content to first 200 chars for compact display + var text = msg.Content.Length > 200 + ? msg.Content[..200] + "…" + : msg.Content; + var ts = ParseTimestamp(msg.Timestamp); + var timeAgo = FormatTimeAgo(ts); + + entries.Add(new AgentActivityEntry(timeAgo, text)); + } + } + catch + { + // Return empty list if gateway is unreachable + } + return entries.Take(Math.Clamp(limit, 1, 20)).ToList(); + } + /// /// Returns the list of available models by reading from the OpenClaw config, /// with fallback to hardcoded list. diff --git a/frontend/src/components/dashboard/AgentModal.vue b/frontend/src/components/dashboard/AgentModal.vue index 8856ff6..350377a 100644 --- a/frontend/src/components/dashboard/AgentModal.vue +++ b/frontend/src/components/dashboard/AgentModal.vue @@ -24,11 +24,19 @@ interface ModelOption { provider: string } +interface ActivityEntry { + time: string + text: string +} + const availableModels = ref([]) const selectedModel = ref('') const currentModel = ref('') const saving = ref(false) +const activityEntries = ref([]) +const activityLoaded = ref(false) + async function loadModels() { try { const res = await fetch('/api/dashboard/models', { credentials: 'include' }) @@ -75,9 +83,23 @@ async function saveModel() { } } +async function loadActivity() { + try { + const res = await fetch(`/api/dashboard/agents/${props.agent.id}/activity?limit=5`, { credentials: 'include' }) + if (res.ok) { + activityEntries.value = await res.json() + } + } catch { + // silent — fallback to empty + } finally { + activityLoaded.value = true + } +} + onMounted(async () => { await loadModels() await loadCurrentModel() + await loadActivity() }) @@ -139,16 +161,35 @@ onMounted(async () => { - Current Task + + Current Task + + + + + + + Thinking… + + {{ agent.currentTask }} - - - - + + + Recent Activity + Loading… + No recent activity + + + {{ entry.time }} + {{ entry.text.length > 100 ? entry.text.slice(0, 100) + '…' : entry.text }} + + + + Goal @@ -384,34 +425,88 @@ onMounted(async () => { transition: width 0.5s ease; } -/* Thinking Dots */ -.thinking-dots { +/* Thinking Indicator */ +.thinking-indicator { display: inline-flex; + align-items: center; gap: 6px; - flex-shrink: 0; + font-size: 9px; + font-weight: 500; + text-transform: none; + letter-spacing: normal; + color: #a78bfa; + margin-left: 8px; + vertical-align: middle; } -.thinking-dot { - width: 7px; - height: 7px; +.thinking-dots-inline { + display: inline-flex; + gap: 4px; +} +.thinking-dots-inline .tdot { + width: 5px; + height: 5px; border-radius: 50%; + background: #a78bfa; + animation: thinking-bounce 1.2s ease-in-out infinite; } -.thinking-dot.blue { - background: #3b82f6; - box-shadow: 0 0 8px #3b82f6; - animation: pulse-dot-blue 1.2s ease-in-out infinite; +.thinking-dots-inline .tdot:nth-child(2) { + animation-delay: 0.2s; } -.thinking-dot.violet { - background: #8b7cf6; - box-shadow: 0 0 8px #8b7cf6; - animation: pulse-dot-violet 1.8s ease-in-out infinite 0.3s; +.thinking-dots-inline .tdot:nth-child(3) { + animation-delay: 0.4s; } -@keyframes pulse-dot-blue { - 0%, 100% { opacity: 0.4; transform: scale(0.7); } - 50% { opacity: 1; transform: scale(1.3); } +@keyframes thinking-bounce { + 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } + 40% { transform: scale(1); opacity: 1; } } -@keyframes pulse-dot-violet { - 0%, 100% { opacity: 0.3; transform: scale(0.6); } - 50% { opacity: 1; transform: scale(1.4); } + +/* Recent Activity */ +.activity-placeholder { + font-size: 10px; + color: #6b7385; + font-style: italic; + padding: 4px 0; +} +.activity-list { + display: flex; + flex-direction: column; + gap: 6px; + max-height: 160px; + overflow-y: auto; +} +.activity-list::-webkit-scrollbar { + width: 4px; +} +.activity-list::-webkit-scrollbar-track { + background: transparent; +} +.activity-list::-webkit-scrollbar-thumb { + background: rgba(139, 124, 246, 0.15); + border-radius: 3px; +} +.activity-item { + display: flex; + flex-direction: column; + gap: 2px; + padding: 6px 10px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.04); +} +.activity-time { + font-size: 9px; + font-weight: 600; + color: #6b7385; + font-variant-numeric: tabular-nums; +} +.activity-text { + font-size: 11px; + line-height: 1.45; + color: #c4c8d4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; } /* Footer */
{{ agent.currentTask }} - - - -