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:
parent
c335bc0962
commit
afeb0f9794
19 changed files with 365 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
19
frontend/packages/swarm-ui/test/agent-types.test.mjs
Normal file
19
frontend/packages/swarm-ui/test/agent-types.test.mjs
Normal 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);
|
||||
});
|
||||
Loading…
Reference in a new issue