Watch
0
0
Fork
You've already forked hyperhive
0

swarm UI: an agent that needs a Claude login shows it on its card

The agent's turn-state header ($SWARM.agent-state.<agent>) gains
`needs_login`, set by hive-agent from its own LoginState, so the flag
reaches the swarm without hive-c0re (mara on #5268: "id prefer the agent
container sending the info it needs login without hive").

- swarm-queue-client: `agent_login::Login`, the shared wire type, beside
  `agent_inference::Effective`. `needs_login: Option<bool>` with
  `#[serde(default)]`: a header from an older agent decodes as `None`
  (unknown), never as "needs login" or "logged in".
- hive-agent: the header publisher reads the shared LoginState cell.
  Every writer of that cell emits a status on the bus afterwards, so a
  login change goes out on the next rebuild, not the 60 s refresh.
- swarm-controller: `agent_login::Reported` follows every agent's header
  and keeps the latest flag; `GET /api/agents/status` merges it into each
  row as `needs_login` (null when unknown).
- swarm-ui: AgentCard shows a warning-tone "login needed" badge when the
  row's `needs_login` is true.

Refs #5268
This commit is contained in:
atlas 2026-10-14 19:32:05 +02:00
commit afeb0f9794
19 changed files with 365 additions and 15 deletions

View file

@ -39,6 +39,15 @@ export interface AgentRow {
snapshot: AgentStatusSnapshot | null;
config_pr: ConfigPrStatus | null;
wanted: Wanted;
// What the agent itself last published about its claude login. `null`:
// no header since swarm-controller started, or an agent that does not
// send the flag — not known, so not "needs login".
needs_login: boolean | null;
}
// Only an agent that said so gets the badge; `null` shows nothing.
export function showsLoginNeeded(row: Pick<AgentRow, "needs_login">): boolean {
return row.needs_login === true;
}
// Same tone pairing as HivesPage — one freshness enum shared by both

View file

@ -17,7 +17,7 @@ import type { ProblemDetails } from "@hive/shared/api-error.js";
import { Card } from "../../ui/card/Card.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import { AgentIcon } from "../AgentIcon.js";
import { FRESHNESS, type AgentRow } from "../AgentTypes.js";
import { FRESHNESS, showsLoginNeeded, type AgentRow } from "../AgentTypes.js";
import { WantedMenu } from "./WantedMenu.js";
import "./AgentCard.css";
@ -71,6 +71,13 @@ export function AgentCard({
</>
}
/>
{showsLoginNeeded(row) ? (
<Badge
tone="warning"
value="login needed"
title="the agent's claude has no session; its turns wait until it is logged in"
/>
) : null}
<span
class="ui-agent-card-wanted"
onClick={(e) => e.stopPropagation()}

View file

@ -46,6 +46,8 @@ export interface AgentStateHeader {
context_window_tokens: number;
ctx_usage: TokenUsage | null;
cost_usage: TokenUsage | null;
// Absent from an agent that predates the field.
needs_login?: boolean;
}
export interface UseSwarmAgentStateStreamResult {

View file

@ -0,0 +1,19 @@
// The roster's "login needed" badge rule. Run by `npm test`, which Node runs
// with its built-in test runner and type stripping, so the `.ts` module is
// imported as it is.
import assert from "node:assert/strict";
import { test } from "node:test";
import { showsLoginNeeded } from "../src/agent/AgentTypes.ts";
test("an agent that says it needs a login gets the badge", () => {
assert.equal(showsLoginNeeded({ needs_login: true }), true);
});
test("an agent that is logged in gets none", () => {
assert.equal(showsLoginNeeded({ needs_login: false }), false);
});
test("an agent that has not said gets none", () => {
assert.equal(showsLoginNeeded({ needs_login: null }), false);
assert.equal(showsLoginNeeded({}), false);
});