From 3b66f0c8d64ea86991231893f7833b03cee1a56f Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 11 Sep 2026 22:21:49 +0200 Subject: [PATCH] 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. --- .../packages/swarm-ui/src/pages/AgentsPage.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx index 0ea72450..7f9e296c 100644 --- a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx @@ -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(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(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(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)} /> )) )}