feat(dash): show active model badge on agent cards (closes #2069)

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<String>` 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)
This commit is contained in:
iris 2026-06-27 19:40:37 +02:00
commit 4375ab6246
4 changed files with 42 additions and 0 deletions

View file

@ -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);

View file

@ -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',
{

View file

@ -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<String>,
/// 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<String>,
}
/// Build the full container list. Wraps `lifecycle::list()` and
@ -78,6 +84,14 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
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<ContainerView> {
deployed_sha,
pending_reminders,
parent,
active_model,
});
}
out
@ -190,6 +205,20 @@ pub async fn read_agent_status_live(name: &str) -> (Option<String>, Option<i64>,
(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<String> {
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

View file

@ -205,6 +205,7 @@ mod tests {
deployed_sha: None,
pending_reminders: 0,
parent: None,
active_model: None,
}
}