Watch
0
0
Fork
You've already forked hyperhive
0

swarm ui: the agent's status message shows how long ago it was set

The card and its detail showed the message alone, so a message from an hour
ago read like the current one. Both now add the age, (5m ago), which ticks
like the other relative times. An agent with no message still shows a dash.
This commit is contained in:
iris 2026-11-10 18:51:45 +01:00 • committed by mara
commit 0547081138
6 changed files with 85 additions and 3 deletions

View file

@ -78,7 +78,7 @@ action that asks first.
`/agents` is a column of separate cards: a filter card, then one
card per agent, none of them inside a bigger card. A card's
header shows its icon, name, the status message, a **terminal**
header shows its icon, name, the status message with how long ago the agent set it (`5m ago`), a **terminal**
button that opens the agent's terminal tab, the **wanted** menu (up, offline,
paused, destroy), a **paused by hive** badge when the agent's own harness paused
its turn loop (hover for the reason; set the agent **up** to resume it), and a

View file

@ -19,6 +19,7 @@ import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import type { AgentActions } from "../roster/AgentActions.js";
import { AgentClaudeLogin } from "./AgentClaudeLogin.js";
import { AgentInference } from "./AgentInference.js";
import { StatusMessage } from "./StatusMessage.js";
import { AgentInstructions } from "./AgentInstructions.js";
import { AgentLimits } from "./AgentLimits.js";
import {
@ -80,7 +81,9 @@ function HeaderRows({
/>
</dd>
<dt>message</dt>
<dd>{row.snapshot?.status_text ?? "—"}</dd>
<dd>
<StatusMessage snapshot={row.snapshot} />
</dd>
<dt>wanted</dt>
<dd>
{/* Full menu (destroy included) — mara: "destroy is already

View file

@ -0,0 +1,26 @@
// <StatusMessage> — an agent's status message with how long ago it set it,
// `working on #1 (5m ago)`. The age ticks, so a card left open stays true. An
// agent with no message shows a dash and no age.
import type { AgentStatusSnapshot } from "../AgentTypes.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import { statusSetAtMs, statusText } from "./statusMessage.js";
export function StatusMessage({
snapshot,
}: {
snapshot: AgentStatusSnapshot | null;
}) {
const text = statusText(snapshot);
const at = statusSetAtMs(snapshot);
return (
<>
{text}
{text !== "—" && at !== null ? (
<>
{" "}
(<RelativeTime epochMs={at} />)
</>
) : null}
</>
);
}

View file

@ -0,0 +1,20 @@
// The agent's status message and when it set it, for the card and its detail.
// Kept out of the component so `npm test` runs it.
import type { AgentStatusSnapshot } from "../AgentTypes.js";
/** When the message was set, in ms since the epoch, or null when the agent has
* set none or the time is not known. The wire carries seconds. */
export function statusSetAtMs(
snapshot: AgentStatusSnapshot | null,
): number | null {
const at = snapshot?.status_set_at;
return typeof at === "number" && Number.isFinite(at) && at > 0
? at * 1000
: null;
}
/** The message, or a dash when there is none. */
export function statusText(snapshot: AgentStatusSnapshot | null): string {
const text = snapshot?.status_text;
return text === null || text === undefined || text === "" ? "—" : text;
}

View file

@ -21,6 +21,7 @@ import { useDynamicTabs } from "../../shell/useDynamicTabs.js";
import { AgentIcon } from "../AgentIcon.js";
import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js";
import { AgentDetail } from "../detail/AgentDetail.js";
import { StatusMessage } from "../detail/StatusMessage.js";
import { termPaths } from "../terminal/term-paths.js";
import {
hivePauseReason,
@ -126,7 +127,7 @@ export function AgentCard({
) : null}
</span>
}
message={row.snapshot?.status_text ?? "—"}
message={<StatusMessage snapshot={row.snapshot} />}
chevron={<span class="ui-agent-card-chevron" aria-hidden="true" />}
/>
{open ? (

View file

@ -0,0 +1,32 @@
// The agent status message and its age. 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 {
statusSetAtMs,
statusText,
} from "../src/agent/detail/statusMessage.ts";
const snap = (status_text, status_set_at) => ({
status_text,
status_set_at,
running: true,
});
test("the set time is read from seconds to ms, or is nothing", () => {
assert.equal(
statusSetAtMs(snap("working", 1_731_000_000)),
1_731_000_000_000,
);
assert.equal(statusSetAtMs(snap("working", null)), null);
assert.equal(statusSetAtMs(snap("working", 0)), null);
assert.equal(statusSetAtMs(null), null);
});
test("an agent with no message shows a dash", () => {
assert.equal(statusText(snap("fixing a bug", 1)), "fixing a bug");
assert.equal(statusText(snap(null, 1)), "—");
assert.equal(statusText(snap("", 1)), "—");
assert.equal(statusText(null), "—");
});