diff --git a/backend/Controllers/DashboardController.cs b/backend/Controllers/DashboardController.cs index 6672ada..38b0085 100644 --- a/backend/Controllers/DashboardController.cs +++ b/backend/Controllers/DashboardController.cs @@ -345,16 +345,12 @@ public class DashboardController( /// /// Returns the list of available models that can be assigned to agents. + /// Reads from OpenClaw config dynamically, falls back to hardcoded list. /// [HttpGet("models")] public ActionResult> GetAvailableModels() { - var models = new List - { - new ModelOption("openai/gpt-5.4", "GPT-5.4", "openai"), - new ModelOption("deepseek/deepseek-v4-flash", "DeepSeek V4 Flash", "deepseek"), - new ModelOption("deepseek/deepseek-v4-pro", "DeepSeek V4 Pro", "deepseek") - }; + var models = gateway.GetAvailableModels(); return Ok(models); } diff --git a/backend/Models/Dashboard.cs b/backend/Models/Dashboard.cs index d611bff..09f7c69 100644 --- a/backend/Models/Dashboard.cs +++ b/backend/Models/Dashboard.cs @@ -8,7 +8,10 @@ public sealed record DashboardAgentInfo( bool IsActive, string? CurrentTask, string? Description, - string[] Tags + string[] Tags, + int Progress = 0, + int Workload = 0, + string? Goal = null ); public sealed record MessageEntry( diff --git a/backend/Services/OpenClawGatewayClient.cs b/backend/Services/OpenClawGatewayClient.cs index 4f4cf87..cc02760 100644 --- a/backend/Services/OpenClawGatewayClient.cs +++ b/backend/Services/OpenClawGatewayClient.cs @@ -119,6 +119,11 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration var agentIds = LoadAgentIdsFromConfig(); var agents = new List(); + + // Read queue once for workload calculation + var queueItems = new List(); + try { queueItems = await GetQueueAsync(); } catch { } + foreach (var id in agentIds) { // Skip the "main" agent (it's the default assistant, not a sub-agent) @@ -177,6 +182,15 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration }; } + // 6. Read goal from workspace files + var goal = await ReadAgentGoalAsync(id); + + // 7. Calculate progress dynamically + var progress = CalculateAgentProgress(id, isActive, status); + + // 8. Calculate workload from queue items + var workload = CalculateAgentWorkload(id, queueItems); + agents.Add(new DashboardAgentInfo( Id: id, Name: string.IsNullOrWhiteSpace(name) ? DeriveRole(id) : name, @@ -185,7 +199,10 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration IsActive: isActive, CurrentTask: currentTask, Description: description, - Tags: tags + Tags: tags, + Progress: progress, + Workload: workload, + Goal: goal )); } return agents; @@ -770,6 +787,241 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration } } + /// + /// Reads the agent's goal from workspace files (goals.md preferred, then AGENTS.md, SOUL.md). + /// Returns the first meaningful line or null. + /// + private async Task ReadAgentGoalAsync(string agentId) + { + try + { + var workspacePath = "/home/node/.openclaw/workspace-" + agentId; + + // 1. Try goals.md first + var goalsPath = Path.Combine(workspacePath, "goals.md"); + if (System.IO.File.Exists(goalsPath)) + { + var content = await System.IO.File.ReadAllTextAsync(goalsPath); + foreach (var line in content.Split('\n')) + { + var trimmed = line.Trim(); + if (!string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('#') && !trimmed.StartsWith('-')) + return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed; + } + } + + // 2. Try SOUL.md "## Rolle" section + var soulPath = Path.Combine(workspacePath, "SOUL.md"); + if (System.IO.File.Exists(soulPath)) + { + var soul = await System.IO.File.ReadAllTextAsync(soulPath); + using var reader = new StringReader(soul); + string? line; + var inRoleSection = false; + while ((line = reader.ReadLine()) is not null) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith("## ", StringComparison.OrdinalIgnoreCase)) + { + if (inRoleSection) break; + if (trimmed.IndexOf("Rolle", StringComparison.OrdinalIgnoreCase) >= 0 + || trimmed.IndexOf("Role", StringComparison.OrdinalIgnoreCase) >= 0 + || trimmed.IndexOf("Oberstes", StringComparison.OrdinalIgnoreCase) >= 0) + { + inRoleSection = true; + } + continue; + } + if (inRoleSection && !string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('-')) + { + return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed; + } + } + + // 3. Look for "## Oberstes Prinzip" as second choice + inRoleSection = false; + reader = new StringReader(soul); + while ((line = reader.ReadLine()) is not null) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith("## ") && trimmed.IndexOf("Prinzip", StringComparison.OrdinalIgnoreCase) >= 0) + { + inRoleSection = true; + continue; + } + if (inRoleSection && !string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('-') && !trimmed.StartsWith('#')) + { + return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed; + } + } + } + + // 4. Try AGENTS.md first heading + var agentsPath = Path.Combine(workspacePath, "AGENTS.md"); + if (System.IO.File.Exists(agentsPath)) + { + var agentsContent = await System.IO.File.ReadAllTextAsync(agentsPath); + foreach (var line in agentsContent.Split('\n')) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith("# ")) + return trimmed[2..].Trim(); + } + } + } + catch + { + // Fallback to hardcoded + } + + // Fallback: known goals + return agentId.ToLowerInvariant() switch + { + "iris" => "Mission Control — maximale Autonomie bei kontrolliertem Risiko", + "programmer" => "Nexus Dashboard & Dungeon System", + "reviewer" => "Zero critical findings before merge", + "architekt" => "Stabile Zero-Downtime-Deployments", + "executor" => "Sichere Host-Execution im Allowlist-Rahmen", + "researcher" => "Verifizierte, strukturierte Recherche-Ergebnisse", + _ => null + }; + } + + /// + /// Calculates agent progress (0-100) based on session activity. + /// Active agents with recent activity get higher scores. + /// Idle agents or inactive sessions get lower scores. + /// + private static int CalculateAgentProgress(string agentId, bool isActive, JsonNode? status) + { + if (!isActive) + return 0; + + // Base progress from active status + var progress = 50; + + // Boost for agents that have a current task + var hasTask = status?["currentTask"]?.GetValue() is not null + || status?["task"]?.GetValue() is not null; + if (hasTask) + progress += 20; + + // Check for last activity timestamp — more recent = higher progress + var lastActivity = status?["lastActivity"]?.GetValue() + ?? status?["lastMessage"]?.GetValue(); + if (lastActivity is not null && DateTimeOffset.TryParse(lastActivity, out var lastTs)) + { + var minutesSinceActivity = (DateTimeOffset.UtcNow - lastTs).TotalMinutes; + if (minutesSinceActivity < 1) + progress += 25; // actively working + else if (minutesSinceActivity < 5) + progress += 15; + else if (minutesSinceActivity < 15) + progress += 10; + else + progress -= 10; // stale + } + else if (hasTask) + { + // Has task but no timestamp — assume actively working + progress += 15; + } + + return Math.Clamp(progress, 0, 100); + } + + /// + /// Calculates agent workload (0-100) based on queue items and active status. + /// More queued tasks or active sessions = higher workload. + /// + private static int CalculateAgentWorkload(string agentId, List queueItems) + { + if (queueItems.Count == 0) + return 0; + + // Calculate workload based on queue density per agent + var agentQueued = queueItems.Count(q => + q.Name.Contains(agentId, StringComparison.OrdinalIgnoreCase) + || q.Id.Contains(agentId, StringComparison.OrdinalIgnoreCase)); + + // Base workload from total queue pressure + var totalQueuePressure = Math.Min(queueItems.Count * 10, 60); + + // Agent-specific queue items add extra weight + var agentPressure = agentQueued * 25; + + return Math.Clamp(totalQueuePressure + agentPressure, 0, 100); + } + + /// + /// Returns the list of available models by reading from the OpenClaw config, + /// with fallback to hardcoded list. + /// + public List GetAvailableModels() + { + try + { + var configPath = configuration.GetValue("AgentConfigPath") + ?? "/home/node/.openclaw/openclaw.json"; + + if (!System.IO.File.Exists(configPath)) + return GetDefaultModels(); + + var json = System.IO.File.ReadAllText(configPath); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Read models from agents.defaults.models + if (!root.TryGetProperty("agents", out var agentsEl)) + return GetDefaultModels(); + if (!agentsEl.TryGetProperty("defaults", out var defaultsEl)) + return GetDefaultModels(); + if (!defaultsEl.TryGetProperty("models", out var modelsEl)) + return GetDefaultModels(); + + var models = new List(); + foreach (var modelProp in modelsEl.EnumerateObject()) + { + var modelId = modelProp.Name; + var modelObj = modelProp.Value; + + var name = modelId; // fallback: use model ID as name + var provider = ExtractProvider(modelId); + + // Check for alias in the model object + if (modelObj.TryGetProperty("alias", out var aliasEl)) + { + var alias = aliasEl.GetString(); + if (!string.IsNullOrWhiteSpace(alias)) + name = alias; + } + + models.Add(new ModelOption(modelId, name, provider)); + } + + return models.Count > 0 ? models : GetDefaultModels(); + } + catch + { + return GetDefaultModels(); + } + } + + private static List GetDefaultModels() + => new() + { + new ModelOption("openai/gpt-5.4", "GPT-5.4", "openai"), + new ModelOption("deepseek/deepseek-v4-flash", "DeepSeek V4 Flash", "deepseek"), + new ModelOption("deepseek/deepseek-v4-pro", "DeepSeek V4 Pro", "deepseek"), + new ModelOption("openai/gpt-5.5", "GPT-5.5", "openai") + }; + + private static string ExtractProvider(string modelId) + { + var slash = modelId.IndexOf('/'); + return slash > 0 ? modelId[..slash] : "unknown"; + } + private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch { "iris" => "Chief of Staff", diff --git a/frontend/src/composables/useDashboardData.ts b/frontend/src/composables/useDashboardData.ts index 54dcae5..f4ad4d2 100644 --- a/frontend/src/composables/useDashboardData.ts +++ b/frontend/src/composables/useDashboardData.ts @@ -77,6 +77,9 @@ interface DashboardAgentInfo { currentTask: string description?: string tags?: string[] + progress?: number + workload?: number + goal?: string | null } interface DashboardOperationEntry { @@ -217,9 +220,10 @@ function enrichAgent(api: DashboardAgentInfo): AgentNodeData { color: catalog.color ?? '#6b7385', icon: catalog.icon ?? 'bot', hero: catalog.hero ?? false, - goal: catalog.goal ?? 'No goal set', - progress: catalog.progress ?? 0, - workload: catalog.workload ?? 0, + // Use API-provided values with catalog fallback for metrics + goal: api.goal ?? catalog.goal ?? 'No goal set', + progress: api.progress ?? catalog.progress ?? 0, + workload: api.workload ?? catalog.workload ?? 0, runtimeSeconds: 0, workingFeed: catalog.workingFeed ?? [], thinkingStream: catalog.thinkingStream ?? [],