dashboard: agent selection + bulk action bar (#443)
per mara on #443: "dont show all the restart buttons etc., just show state and links. instead, clicking an agent icon selects that agent. you can select as many as you like. then you can run an action on all of them." selection model - module-level selectionState = Set<string> of agent logical names. clicking a container-row icon toggles membership; Esc clears the whole set (ignored when an editable element has focus so typing in compose / answer / journal-search isn't intercepted). - icon now has role="button" + tabindex="0" so it's keyboard-accessible; aria-pressed reflects the toggle state. hover + focus-visible get a mauve ring. - selected rows get a .selected class — mauve outline + faint mauve wash on the row, brighter ring on the icon. - on every renderContainers pass, stale selections (agents destroyed while selected) are pruned defensively. sticky action bar - new #selection-bar in index.html — fixed bottom strip with count chip, name list, action buttons, clear button. hidden when selection empty. styled to match the flow composer's frosted-mauve chrome (vibecore family). - per mara's option B answer: every action button is visible; buttons that don't apply to the whole selection are disabled with a tooltip explaining WHY ("iris is already stopped" etc). .btn:disabled styling added. - actions: R3ST4RT / ST0P / ST4RT / R3BU1LD / DESTR0Y / PURG3. per-action confirm prompt lists the names being acted on. when the selection includes the manager the ST0P prompt calls out the consequence (approvals + meta operations pause until manager is restarted). DESTR0Y/PURG3 stay sub-agent-only; including the manager disables them with a clear tooltip. - actions POST per agent in a loop to the existing endpoints (/restart/{name}, /kill/{name}, etc.); no new backend wire surface. event-driven derived stores (containersState, rebuildQueueState) already update live via SSE — no manual refetch. - failures from any individual POST surface in a single alert at the end rather than spamming dialogs mid-loop. per-card actions removed - the in-card R3ST4RT / ST0P / ST4RT / R3BU1LD / DESTR0Y / PURG3 block is gone. cards now show identity / state / nav-strip / status text / drill-ins only — "state and links" per mara. the contextual needs-update chip in the head row stays. manager stop - the "also make manager stoppable" half of #443 ships as PR #445 (separate small backend change). this PR depends on #445 for the bulk-stop button to actually work on selections containing the manager; until #445 merges, ST0P on a manager-included selection will fail individually with a 500 for the manager (other selected agents are still stopped; failures bucket into the end-of-loop alert). build clean: npm run build --workspace=@hive/dashboard. no backend changes here.
This commit is contained in:
parent
b42b219f9b
commit
a366c00e0e
3 changed files with 336 additions and 42 deletions
|
|
@ -298,6 +298,28 @@ a:hover {
|
|||
aspect-ratio: 1;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(17, 17, 27, 0.6);
|
||||
/* #443 — icon is the selection toggle. Cursor + hover ring make
|
||||
that affordable without a chrome change. The :focus-visible ring
|
||||
covers keyboard activation (Enter / Space). */
|
||||
cursor: pointer;
|
||||
transition: box-shadow 120ms ease, transform 120ms ease;
|
||||
}
|
||||
.container-row:not(.tombstone) > .container-icon:hover {
|
||||
box-shadow: 0 0 0 2px var(--purple);
|
||||
}
|
||||
.container-row:not(.tombstone) > .container-icon:focus-visible {
|
||||
outline: 2px solid var(--purple);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
/* Selected state — mauve ring on the icon + a matching outline on the
|
||||
whole row so a glance at the list shows what's in the active set. */
|
||||
.container-row.selected {
|
||||
border-color: var(--purple);
|
||||
box-shadow: inset 0 0 0 1px var(--purple);
|
||||
background: rgba(203, 166, 247, 0.06);
|
||||
}
|
||||
.container-row.selected > .container-icon {
|
||||
box-shadow: 0 0 0 2px var(--purple), 0 0 12px -4px var(--purple);
|
||||
}
|
||||
/* The icon image fills the square wrapper and is taken out of flow
|
||||
(absolute) so its load state — pending, loaded, broken — can never
|
||||
|
|
@ -806,6 +828,19 @@ ul form.inline { display: inline-block; }
|
|||
text-shadow: 0 0 10px currentColor;
|
||||
box-shadow: 0 0 10px -2px currentColor;
|
||||
}
|
||||
.btn:disabled,
|
||||
.btn[disabled] {
|
||||
opacity: 0.32;
|
||||
cursor: not-allowed;
|
||||
text-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn:disabled:hover,
|
||||
.btn[disabled]:hover {
|
||||
background: transparent;
|
||||
text-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn-approve { color: var(--green); border-color: var(--green); }
|
||||
.btn-deny { color: var(--red); border-color: var(--red); }
|
||||
.btn-destroy { color: var(--red); border-color: var(--red); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
||||
|
|
@ -1519,3 +1554,63 @@ body.flow-shell .tabbar .tab.active.tab-link {
|
|||
wants `#inbox-section` in the DOM (legacy contract), but we
|
||||
surface the messages via the pill/flyout instead. */
|
||||
.flow-inbox-headless { display: none !important; }
|
||||
|
||||
/* Selection bar (#443). Sticky-bottom strip that surfaces bulk
|
||||
actions when ≥1 agent is selected (click the icon). Visually
|
||||
echoes the flow composer's frosted-mauve treatment so the chrome
|
||||
reads as part of the same vibecore family. Hidden when empty —
|
||||
leaves the page footer's normal-flow position untouched. */
|
||||
.selection-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6em;
|
||||
padding: 0.55em 1em;
|
||||
background: var(--flow-frost-bg);
|
||||
-webkit-backdrop-filter: var(--flow-frost-blur);
|
||||
backdrop-filter: var(--flow-frost-blur);
|
||||
border-top: 1px solid var(--purple);
|
||||
box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.selection-bar[hidden] { display: none; }
|
||||
.selection-count {
|
||||
color: var(--purple);
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.selection-names {
|
||||
color: var(--muted);
|
||||
font-size: 0.85em;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.selection-actions {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4em;
|
||||
align-items: center;
|
||||
}
|
||||
.selection-actions .btn {
|
||||
font-size: 0.75em;
|
||||
padding: 0.2em 0.7em;
|
||||
}
|
||||
.selection-clear {
|
||||
color: var(--muted);
|
||||
border-color: var(--purple-dim);
|
||||
font-size: 0.75em;
|
||||
padding: 0.2em 0.7em;
|
||||
}
|
||||
/* Pad the dashboard body so the sticky bar doesn't cover the
|
||||
bottom of the agent list. ~3.4em covers the bar's vertical
|
||||
footprint with breathing room; only applies when the bar is
|
||||
visible (`body.has-selection`, toggled by app.js when the
|
||||
selection set is non-empty). */
|
||||
body.dashboard-shell.has-selection { padding-bottom: 4.5em; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue