From 1656b265ed2247710d17e9f58c31835b7f452940 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 31 May 2026 10:13:30 +0200 Subject: [PATCH] dashboard #486: drop manager special-case + enable M0V3 picker for multi-select (mara on #695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes from mara's review: 1. drop the manager special-case. Both M0V3 affordances now apply regardless of whether the manager is in the selection; backend topology::set_parent refuses the manager move and the failure surfaces in the bulk-action error roll-up. Matches the #443 ST0P policy of 'don't pre-gate manager actions, let the backend speak'. 2. enable the M0V3 → picker for multi-select. Was single-agent only in v1. Picker now omits every selected agent itself plus the union of every selected agent's descendants (cycle-safe across the whole batch); on submit POSTs once per selected agent sequentially, same shape as the existing bulk-button loop. Confirm message + error roll-up adapt to selection size. docs/web-ui.md updated to match. --- docs/web-ui.md | 21 ++-- frontend/packages/dashboard/src/tabs.js | 148 +++++++++++++----------- 2 files changed, 92 insertions(+), 77 deletions(-) diff --git a/docs/web-ui.md b/docs/web-ui.md index a4a39df6..8248a034 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -441,15 +441,18 @@ frosted-mauve bar slides up from the bottom of the viewport - `↻ R3BU1LD` — always available - `DESTR0Y` / `PURG3` — sub-agents only (disabled if manager selected) - `⇡ M0V3 → ROOT` (#486) — promote selected agents to top-level - (parent = null); disabled when all selected are already at root - or when the selection includes the manager. - - `⇢ M0V3 → [select]` (#486) — single-agent-only inline picker; - the dropdown lists every container that isn't the target itself - nor a descendant of it (client-side cycle prevention; the - backend's `topology::set_parent` re-checks). On submit POSTs to - `/api/topology/set-parent` (form-encoded `child=&new_parent=`), - which writes `topology.json` and re-emits a container snapshot so - the tree repaints without a page reload. + (parent = null); disabled when all selected are already at root. + Manager included with no special-case (matches the `ST0P` policy); + the backend's `topology::set_parent` refuses to move the manager + and the refusal surfaces in the failure roll-up. + - `⇢ M0V3 → [select]` (#486) — inline picker available for any + selection size. The dropdown lists every container that isn't IN + the selection itself nor a descendant of any selected agent + (client-side BFS cycle prevention across the whole batch; the + backend re-checks per-agent). On submit POSTs to + `/api/topology/set-parent` (form-encoded `child=&new_parent=`) + once per selected agent, which writes `topology.json` and re-emits + a container snapshot so the tree repaints without a page reload. - **`✕ clear`** button + `Esc` key clear the entire selection. Stale selections (agents destroyed while selected) are pruned on diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index b06791c8..a8276669 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -883,113 +883,125 @@ window.marked = marked; // #486 — render the M0V3 affordances inside the selection bar. Split // into its own helper because the picker variant needs a select + button // pair, not the single-button shape addBulkButton ships. + // + // No client-side manager special-case (mara on #695): backend + // `topology::set_parent` refuses to move the manager and surfaces the + // refusal as a per-agent failure in the bulk-action error roll-up. Same + // pattern as #443 ST0P (which also doesn't special-case manager). function addMoveActions(parent, selected, containers) { - // Manager can never be a child of anything — backend refuses with - // "refusing to move the manager". Disable both affordances when the - // selection includes hm1nd (or any agent flagged is_manager). - const managerNames = selected.filter((c) => c.is_manager).map((c) => c.name); - const movable = !managerNames.length; - // M0V3 → ROOT: parent=null for every selected agent. Only meaningful // when at least one selected agent currently has a non-null parent; // otherwise it's a no-op for everything. const someNotAtRoot = selected.some((c) => c.parent); - addBulkButton(parent, 'btn-move', '⇡ M0V3 → ROOT', movable && someNotAtRoot, selected, { + addBulkButton(parent, 'btn-move', '⇡ M0V3 → ROOT', someNotAtRoot, selected, { action: '/api/topology/set-parent', perAgentBodyFor: (name) => ({ child: name, new_parent: '' }), confirm: (names) => `promote ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')}) to top-level (parent → root)?`, - disabledTitle: !movable - ? why('⇡ M0V3 → ROOT', managerNames.map((n) => `\`${n}\` is the manager`)) - : (!someNotAtRoot ? '⇡ M0V3 → ROOT not available — all selected agents are already at root' : null), + disabledTitle: !someNotAtRoot + ? '⇡ M0V3 → ROOT not available — all selected agents are already at root' + : null, }); - // M0V3 → : only when exactly one agent is selected. Picker - // omits self + own descendants (cycle-safe) + the manager (the - // backend allows manager-as-parent, that's fine — manager IS the - // default parent already, so we include it as an option). Empty - // candidate list ⇒ disable the picker. - if (selected.length !== 1 || !movable) { - // Multi-select or manager-included → skip the picker. The - // ROOT button above still applies; the picker is single-agent - // ergonomics only. - return; - } - const target = selected[0]; - const candidates = validReparentCandidates(target, containers); - + // M0V3 → : inline `