From 4375ab6246083b5105440dd5e90b2dab7c685aaa Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 27 Jun 2026 19:40:37 +0200 Subject: [PATCH] feat(dash): show active model badge on agent cards (closes #2069) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read the persisted model name from each agent's harness state file (harness/hyperhive-model) and surface it as a small blue badge on the container row in the SW4RM tab. - container_view.rs: add `active_model: Option` to ContainerView; populated by new `read_active_model` helper that reads harness/hyperhive-model; only set when container is running (stale model info from a stopped agent is misleading) - container_view.rs: add active_model to ContainerView literal in host_stats test helper - tabs.js: render badge-model chip after needs-update, before reminders; add active_model to the row fingerprint so re-renders fire on model change - common.css: add .badge-model (blue, 80% opacity — informational) --- frontend/packages/dashboard/src/common.css | 5 ++++ frontend/packages/dashboard/src/tabs.js | 7 ++++++ hive-c0re/src/container_view.rs | 29 ++++++++++++++++++++++ hive-c0re/src/host_stats.rs | 1 + 4 files changed, 42 insertions(+) diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css index ea353cbe..ec677ed0 100644 --- a/frontend/packages/dashboard/src/common.css +++ b/frontend/packages/dashboard/src/common.css @@ -85,6 +85,11 @@ code { color: var(--purple); border-color: var(--purple); text-shadow: 0 0 6px color-mix(in srgb, var(--purple) 40%, transparent); } +/* Active Claude model badge on dashboard container rows. */ +.badge-model { + color: var(--blue); border-color: var(--blue); + opacity: 0.8; +} /* Context-window usage badges on dashboard container rows. */ .badge-ctx-ok { color: var(--green); border-color: var(--green); diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 8b9f4c5a..6220ac44 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -521,6 +521,7 @@ window.marked = marked; needs_login: c.needs_login, needs_update: c.needs_update, pending_reminders: c.pending_reminders, + active_model: c.active_model, port: c.port, pending, opRunning, @@ -726,6 +727,12 @@ window.marked = marked; )); } + if (c.active_model) { + head.append(el('span', + { class: 'badge badge-model', title: `active claude model: ${c.active_model}` }, + c.active_model)); + } + if (c.pending_reminders && c.pending_reminders > 0) { head.append(el('span', { diff --git a/hive-c0re/src/container_view.rs b/hive-c0re/src/container_view.rs index 28821cda..7d1d6ea5 100644 --- a/hive-c0re/src/container_view.rs +++ b/hive-c0re/src/container_view.rs @@ -44,6 +44,12 @@ pub struct ContainerView { /// in the tree. See `docs/agent-hierarchy.md::Current state`. #[serde(default, skip_serializing_if = "Option::is_none")] pub parent: Option, + /// The Claude model the agent's harness is currently using, read from + /// `harness/hyperhive-model`. `None` when the agent has never started + /// a turn or the file is absent. Only meaningful when `running` is + /// true; the dashboard skips the badge for stopped containers. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub active_model: Option, } /// Build the full container list. Wraps `lifecycle::list()` and @@ -78,6 +84,14 @@ pub async fn build_all(coord: &Coordinator) -> Vec { let needs_login = running && (!claude_has_session(&Coordinator::agent_claude_dir(&logical)) || auth_failed_sentinel(&logical)); + // Read the active model from the harness state file. Only surfaced + // when the container is running — stale model info from a stopped + // agent is misleading (the model may change on next boot). + let active_model = if running { + read_active_model(&logical) + } else { + None + }; out.push(ContainerView { port: lifecycle::agent_web_port(&logical), running, @@ -88,6 +102,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec { deployed_sha, pending_reminders, parent, + active_model, }); } out @@ -190,6 +205,20 @@ pub async fn read_agent_status_live(name: &str) -> (Option, Option, (text, set_at, true) } +/// Read the active Claude model from the agent's harness state file +/// (`harness/hyperhive-model`). Returns `None` when the file is absent +/// or empty — callers should treat `None` as "model not yet known". +fn read_active_model(name: &str) -> Option { + let path = Coordinator::agent_harness_dir(name).join("hyperhive-model"); + let s = std::fs::read_to_string(path).ok()?; + let trimmed = s.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_owned()) + } +} + /// Host-side hive + swarm display names, read from the c0re service's /// own process env. The `hive-c0re.nix` module sets these from /// `services.hyperhive.{hiveName, swarmName}`. The agent-side diff --git a/hive-c0re/src/host_stats.rs b/hive-c0re/src/host_stats.rs index 819c90d0..4e16da55 100644 --- a/hive-c0re/src/host_stats.rs +++ b/hive-c0re/src/host_stats.rs @@ -205,6 +205,7 @@ mod tests { deployed_sha: None, pending_reminders: 0, parent: None, + active_model: None, } }