diff --git a/frontend/packages/swarm-ui/src/agent/picker/AgentPicker.css b/frontend/packages/swarm-ui/src/agent/picker/AgentPicker.css new file mode 100644 index 000000000..52a427ea6 --- /dev/null +++ b/frontend/packages/swarm-ui/src/agent/picker/AgentPicker.css @@ -0,0 +1,51 @@ +/* : each agent a labelled checkbox card, icon over name; the grid + wraps to however many cards fit. */ +.ui-agent-picker { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(8em, 1fr)); + gap: 0.6em; + list-style: none; + margin: 0; + padding: 0; +} +.ui-agent-picker-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35em; + padding: 0.7em 0.5em 0.6em; + border: 1px solid var(--border); + border-radius: 0.5em; + background: var(--bg); + cursor: pointer; +} +.ui-agent-picker-card:hover { + border-color: var(--muted); +} +.ui-agent-picker-card-on { + border-color: var(--purple); + background: var(--bg-elev); +} +.ui-agent-picker-card:focus-within { + outline: 2px solid var(--purple); + outline-offset: 2px; +} +.ui-agent-picker-check { + position: absolute; + top: 0.5em; + left: 0.5em; + margin: 0; +} +.ui-agent-picker-name { + max-width: 100%; + overflow-wrap: anywhere; + text-align: center; +} +.ui-agent-picker-state { + color: var(--muted); + font-size: 0.8em; +} +.ui-agent-picker-state-up { + color: var(--green); +} diff --git a/frontend/packages/swarm-ui/src/agent/picker/AgentPicker.tsx b/frontend/packages/swarm-ui/src/agent/picker/AgentPicker.tsx new file mode 100644 index 000000000..788842d9a --- /dev/null +++ b/frontend/packages/swarm-ui/src/agent/picker/AgentPicker.tsx @@ -0,0 +1,63 @@ +// — pick agents from a grid of cards, each a labelled checkbox +// with the agent's icon over its name and, when the caller knows it, a state +// line under that. The broadcast composer and the schedule form share it, so +// choosing agents looks the same wherever the operator does it. It holds no +// state: the caller owns the selection and what a toggle does to it. +import { AgentIcon } from "../AgentIcon.js"; +import "./AgentPicker.css"; + +export interface AgentPickerOption { + name: string; + /** A line under the name, such as "running". Omitted: no line. */ + state?: string; + /** Colours the state line as up. */ + up?: boolean; +} + +export function AgentPicker({ + options, + selected, + onToggle, + label = "agents", +}: { + options: AgentPickerOption[]; + selected: string[]; + onToggle: (name: string) => void; + label?: string; +}) { + return ( + + ); +} diff --git a/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.css b/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.css index e8fa201a8..379616637 100644 --- a/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.css +++ b/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.css @@ -57,57 +57,6 @@ font-size: 0.9em; } -/* The picker: each agent a labelled checkbox card, icon over name. */ -.ui-broadcast-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(8em, 1fr)); - gap: 0.6em; - list-style: none; - margin: 0; - padding: 0; -} -.ui-broadcast-agent { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.35em; - padding: 0.7em 0.5em 0.6em; - border: 1px solid var(--border); - border-radius: 0.5em; - background: var(--bg); - cursor: pointer; -} -.ui-broadcast-agent:hover { - border-color: var(--muted); -} -.ui-broadcast-agent-on { - border-color: var(--purple); - background: var(--bg-elev); -} -.ui-broadcast-agent:focus-within { - outline: 2px solid var(--purple); - outline-offset: 2px; -} -.ui-broadcast-check { - position: absolute; - top: 0.5em; - left: 0.5em; - margin: 0; -} -.ui-broadcast-name { - max-width: 100%; - overflow-wrap: anywhere; - text-align: center; -} -.ui-broadcast-state { - color: var(--muted); - font-size: 0.8em; -} -.ui-broadcast-state-up { - color: var(--green); -} - .ui-broadcast-send { display: flex; align-items: center; diff --git a/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx b/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx index 9d20e6919..571673417 100644 --- a/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx +++ b/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx @@ -16,6 +16,7 @@ import { useState } from "preact/hooks"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { AgentIcon } from "../../agent/AgentIcon.js"; +import { AgentPicker } from "../../agent/picker/AgentPicker.js"; import { Button } from "../../ui/button/Button.js"; import { RefreshIntervalPicker, @@ -217,40 +218,15 @@ export function BroadcastComposer() { : "no agents."} ) : ( - + ({ + name: agent.name, + state: isRunning(agent) ? "running" : "not running", + up: isRunning(agent), + }))} + selected={targets} + onToggle={(name) => setTicked((prev) => toggled(prev, name))} + /> )} setAgents((cur) => cur.includes(a) ? cur.filter((x) => x !== a) : [...cur, a], @@ -199,9 +195,9 @@ export function ScheduleForm({ : "no agents to pick from"} ) : ( - ({ name }))} + selected={agents} onToggle={toggleAgent} /> )}