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:
parent
1cc1cb804d
commit
0547081138
6 changed files with 85 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
20
frontend/packages/swarm-ui/src/agent/detail/statusMessage.ts
Normal file
20
frontend/packages/swarm-ui/src/agent/detail/statusMessage.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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 ? (
|
||||
|
|
|
|||
32
frontend/packages/swarm-ui/test/status-message.test.mjs
Normal file
32
frontend/packages/swarm-ui/test/status-message.test.mjs
Normal 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), "—");
|
||||
});
|
||||
Loading…
Reference in a new issue