import { useEffect, useRef, useState } from "react"; import { Badge, Button, LayerCard, Table, Tooltip } from "@cloudflare/kumo"; import type { Activity, Brand, ProjectState } from "./contracts.ts"; import { agentStatusRows, type AgentStatusRow } from "./agent-status-model.ts"; import { clockTime, oldestLoadedActivity, packetPushes, timestamp } from "./fork-graph-model.ts"; import { Saddlecloth } from "./lane.tsx"; import { relativeTime } from "./time.ts"; import { agentUsageToday } from "./usage.ts"; import { UsageFigures } from "./usage.tsx"; import { FirstRunEmpty } from "./empty.tsx"; function useNow() { const [now, setNow] = useState(Date.now); useEffect(() => { const timer = setInterval(() => setNow(Date.now()), 30_000); return () => clearInterval(timer); }, []); return now; } export function AgentStatusBadge({ row }: { row: AgentStatusRow }) { return row.status ? {row.status.label} : Idle; } function LastPush({ row, now, stacked = false }: { row: AgentStatusRow; now: number; stacked?: boolean }) { return row.lastPush ? }> {row.lastPush.after.slice(0, 7)}{!stacked && " · "} : {row.missingPush}; } function Fork({ value }: { value?: string }) { return value ? }>{value} : null; } export function AgentStatusTable({ state, activity, brand, owner, openPacket, revealRepo }: { state: ProjectState; activity: Activity[]; brand: Brand; owner: boolean; openPacket(id: string): void; revealRepo?: string; }) { const now = useNow(); const [expanded, setExpanded] = useState(false); const rows = agentStatusRows(state, activity, now); const live = rows.filter((row) => row.status); const idle = rows.filter((row) => !row.status); const section = useRef(null); const revealedFork = state.forks.find((fork) => fork.repoName === revealRepo); // Needs you can target an idle fork; reveal that disclosure before moving focus. useEffect(() => { if (revealedFork && !state.claims.some((claim) => claim.active && claim.agent === revealedFork.agent)) setExpanded(true); }, [revealRepo, revealedFork, state.claims]); useEffect(() => { if (!revealRepo) return; const row = [...(section.current?.querySelectorAll("[data-agent-repo]") ?? [])] .find((entry) => entry.dataset.agentRepo === revealRepo); row?.focus(); row?.scrollIntoView({ block: "nearest" }); }, [revealRepo, expanded]); const table = (items: AgentStatusRow[], labelledBy = "agents-heading") =>
{["Agent", "Status", "Packet", "Last push", "Waiting on", "Tokens"].map((heading) => {heading})}{items.map((row) => }>{row.agent.name}
{!row.agent.createdAt && Legacy}
{row.status?.quietSince !== undefined &&

No push since {clockTime(row.status.quietSince)} ({Math.max(0, Math.floor((now - row.status.quietSince) / 60_000))} min)

}
{row.status ? <> openPacket(row.status!.claim.packet)} />}> {row.status.claim.packet} {row.claims.length > 1 && entry.claim.packet).join(", ")} render={}>+{row.claims.length - 1} more} : "—"} {row.status ? <> {row.status.waiting === "owner" ? owner ? "You" : "the owner" : row.status.waiting === "checks" ? "Checks" : row.agent.name} {row.status.caption && (owner || row.status.waiting === "agent") &&

{row.status.caption}

} : "—"}
)}
; return

Agents

{rows.length > 0 &&

Cumulative usage from the latest reports received today, per packet and fork. These snapshots do not measure tokens consumed today.

} {live.length > 0 && table(live)} {idle.length > 0 && <> {expanded &&
{table(idle, "agents-heading idle-agents-toggle")}
} } {!rows.length && }
; } /** Reduced FH-078 card: caller identity and public /state only, never inferred next actions. */ export function ResumeCard({ state, activity, agent }: { state: ProjectState; activity: Activity[]; agent?: string }) { const now = useNow(); if (!agent) return null; const row = agentStatusRows(state, activity, now).find((entry) => entry.agent.name === agent); if (!row) return null; // The card names one assignment and fork; never borrow another fork's last push. const lastPush = row.status ? packetPushes(activity, row.status.claim.repoName, state.project.defaultBranch, timestamp(row.status.claim.createdAt) ?? 0, now).toSorted((a, b) => (timestamp(b.recordedAt) ?? 0) - (timestamp(a.recordedAt) ?? 0) || b.sequence - a.sequence)[0] : undefined; const packet = state.packets.find((entry) => entry.id === row.status?.claim.packet); const missingPush = (timestamp(row.status?.claim.createdAt) ?? -Infinity) > oldestLoadedActivity(activity) ? "No push yet" as const : "Not in loaded activity" as const; return

Resume where you left off

{row.status ?
Packet
{row.status.claim.packet}{packet && ` · ${packet.title}`}
Fork
Last push
Status
:

No live claim.

}
; }