From 8847e6390b9ce02c12657d1c0d34600c1966ad83 Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 6 Nov 2026 19:49:47 +0100 Subject: [PATCH] swarm-ui: the broadcast roster and the egress lists show placeholders while they load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The broadcast grid showed nothing while the agents loaded, and the egress card a 'loading the … domains' line. They now show grey placeholders in the shape of what is coming: six agent chips with the real chip's height (160 px, measured), and three domain rows with their remove buttons. --- docs/2-handbook/swarm/ui.md | 4 ++- .../src/pages/broadcast/BroadcastComposer.css | 13 +++++++++ .../src/pages/broadcast/BroadcastComposer.tsx | 29 ++++++++++++++++++- .../swarm-ui/src/pages/swarm/EgressCard.tsx | 16 ++++++++-- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/docs/2-handbook/swarm/ui.md b/docs/2-handbook/swarm/ui.md index 066313df..0bdb65f3 100644 --- a/docs/2-handbook/swarm/ui.md +++ b/docs/2-handbook/swarm/ui.md @@ -611,6 +611,7 @@ to more than one agent at once, and the messages show up in the list below as th go. Folding it again keeps what you typed and ticked. Pick the agents from a grid of their icons. **running** shows the agents that report themselves running and **all** shows every agent; the browser keeps the choice. +While the agents load, the grid shows grey placeholder chips of the same size. **select all** ticks every agent the filter shows, and **clear** unticks them. The composer skips an agent you ticked and then hid with the filter. Type the message and press **send to N agents**. The text goes exactly as typed, as an @@ -739,7 +740,8 @@ swarm. The tick is off by default, so an entry is for this agent alone. The **egress** card on `/swarm` has two lists of the swarm-wide entries, one per line, with **remove** beside each and an **add** that opens the same dialog without the tick (the -entry is always for every agent). **allowed** is the list above. **denied** is the swarm's +entry is always for every agent). While a list loads, it shows grey placeholder +rows. **allowed** is the list above. **denied** is the swarm's deny list: every egress mode refuses a denied name, even when an allowed entry, a hive's own `allowedDomains` or the unrestricted internet flag would let it through. The card says which hives haven't applied the current deny list yet; until they do, their proxies may still let diff --git a/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.css b/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.css index fbd88b4c..e8fa201a 100644 --- a/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.css +++ b/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.css @@ -157,3 +157,16 @@ .ui-broadcast-fail .ui-broadcast-outcome-name { color: var(--fg); } +/* The loading twin's chip: not a control, so no pointer. */ +.ui-broadcast-agent-loading { + cursor: default; +} +/* Its name and state lines take the height of a line of text, so the chip is + the height of a real one. */ +.ui-broadcast-agent-loading .ui-broadcast-name, +.ui-broadcast-agent-loading .ui-broadcast-state { + display: flex; + align-items: center; + justify-content: center; + min-height: 1.6em; +} diff --git a/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx b/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx index ca58ba1a..359d8643 100644 --- a/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx +++ b/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx @@ -23,6 +23,7 @@ import { } from "../../ui/refresh-interval/RefreshInterval.js"; import { EmptyState } from "../../ui/empty-state/EmptyState.js"; import { Hint } from "../../ui/hint/Hint.js"; +import { Skeleton, SkeletonRegion } from "../../ui/skeleton/Skeleton.js"; import { fetchJson, usePolled } from "../../ui/polled/usePolled.js"; import { TextAreaField } from "../../ui/text-area-field/TextAreaField.js"; import { @@ -59,6 +60,30 @@ function storedFilter(): AgentFilter { } } +// The twin of the agent grid while the roster loads: the same grid and the +// same chip (border, icon, name, state) with a placeholder in each slot. +function RosterSkeleton({ count = 6 }: { count?: number }) { + return ( + + + + ); +} + export function BroadcastComposer() { const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL_MS); @@ -183,7 +208,9 @@ export function BroadcastComposer() { /> - {agents !== null && shown.length === 0 ? ( + {agents === null ? ( + + ) : shown.length === 0 ? ( {filter === "running" && all.length > 0 ? "no agent is running right now. Show all to pick a stopped one." diff --git a/frontend/packages/swarm-ui/src/pages/swarm/EgressCard.tsx b/frontend/packages/swarm-ui/src/pages/swarm/EgressCard.tsx index 3fb165e6..126b34d7 100644 --- a/frontend/packages/swarm-ui/src/pages/swarm/EgressCard.tsx +++ b/frontend/packages/swarm-ui/src/pages/swarm/EgressCard.tsx @@ -26,6 +26,7 @@ import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js"; import { Dialog } from "../../ui/dialog/Dialog.js"; import { Hint } from "../../ui/hint/Hint.js"; import { SectionHeading } from "../../ui/section-heading/SectionHeading.js"; +import { Skeleton, SkeletonRegion } from "../../ui/skeleton/Skeleton.js"; import "../../agent/detail/AgentLimits.css"; import "./SwarmPage.css"; @@ -67,9 +68,18 @@ function DomainSection({ {errorPanel} {entries === null ? ( errorPanel === null ? ( - - loading the {kind === "deny" ? "denied" : "allowed"} domains… - + + + ) : null ) : ( <>