swarm-ui/agents: fix detail panel going stale after refresh or an action

argus, PR review: detailTarget stored the whole AgentRow object at
selection time, so it never picked up a later refresh() or a
declareState patch - the card list updated live, the panel next to it
kept showing whatever was true the moment it was opened. Concrete
repro: open an agent's detail panel, wait for the next refresh or
destroy it from inside the panel itself, watch the panel not update.

Fix: store only the selected agent's name (detailTargetName) and
re-derive the actual row from rows on every render
(rows?.find(r => r.name === detailTargetName)). The panel can't drift
from what the list is showing since it's reading the same array.

Verified with a real refresh cycle against a mock server that returns
different data on the second call: before, both card and panel show
"idle"; after a live refresh, both show the new value in sync.
This commit is contained in:
iris 2026-09-11 22:21:49 +02:00
commit 3b66f0c8d6

View file

@ -506,9 +506,17 @@ export function AgentsPage() {
// confirmation of a `declareState` call, it's an unrelated action with
// its own form (`LinkMatrixAccountForm`).
const [matrixTarget, setMatrixTarget] = useState<AgentRow | null>(null);
// The row showing the detail panel (see the `Dialog` below for what
// it holds) — same null-means-closed shape as the two above.
const [detailTarget, setDetailTarget] = useState<AgentRow | null>(null);
// Which agent the detail panel shows, *by name* — not the `AgentRow`
// object itself. Storing the row would snapshot it at selection time;
// `rows` replaces its whole array on every `refresh()` and every
// `declareState` patch, so a captured object goes stale the moment
// either fires (argus, PR review: "open a detail panel, wait for the
// next refresh or destroy from inside the panel itself — the card
// list updates live, the panel next to it doesn't"). `detailTarget`
// below re-derives the live row from `rows` every render instead, so
// it can't drift from what the list is showing.
const [detailTargetName, setDetailTargetName] = useState<string | null>(null);
const detailTarget = rows?.find((r) => r.name === detailTargetName) ?? null;
async function refresh() {
const res = await fetch("/api/agents/status");
@ -851,11 +859,11 @@ export function AgentsPage() {
row={a}
pending={pendingAgents.has(a.name)}
error={actionErrors.get(a.name)}
selected={detailTarget?.name === a.name}
selected={detailTargetName === a.name}
onSelectUp={selectUp}
onSelectOffline={selectOffline}
onSelectPaused={selectPaused}
onOpenDetail={setDetailTarget}
onOpenDetail={(row) => setDetailTargetName(row.name)}
/>
))
)}