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 ?? [],