diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 2578cbdd..e10073be 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -2520,4 +2520,6 @@ body.dashboard-shell.has-selection { padding-bottom: 4.5em; } opacity: 0.5; cursor: default; } - +.move-picker .btn-move { + margin-left: 0; +} diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 494ecb4d..f99afc04 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -933,49 +933,80 @@ window.marked = marked; disabledTitle: why('PURG3', managerNames.map((n) => `\`${n}\` is the manager`)), }); - // Move agent(s) in the topology tree — selecting an option in the - // M0V3 dropdown immediately confirms + executes the move. "(no parent)" - // promotes to root (empty new_parent on the backend). Cycle-safe: - // dropdown filters out self and descendants on the client side; the - // backend rechecks via `topology::set_parent`. + // Move agent(s) in the topology tree — see + // docs/web-ui.md::Selection bar for the two affordances: // - // Backend: POST /api/topology/set-parent (dashboard.rs), - // form-encoded `child=&new_parent=`. Re-emits - // container snapshots on success so the tree repaints without a - // separate refresh. + // ⇡ M0V3 → ROOT promote selected agent(s) to top-level (parent=null) + // ⇢ M0V3 → [sel] reparent the single selected agent under a picked + // parent (cycle-safe — the dropdown filters out self + // and own descendants on the client side; the + // backend rechecks via `topology::set_parent`). + // + // Backend lives at POST /api/topology/set-parent (dashboard.rs), + // form-encoded `child=&new_parent=`. The + // backend re-emits container snapshots on success, so the tree + // repaints without a separate refresh. addMoveActions(actions, selected, containers); } - // Render the M0V3 picker in the selection bar. Selecting any real option - // (including "(no parent)") immediately fires a confirm + POST — no - // separate button. Backend `topology::set_parent` refuses invalid moves - // and the refusal surfaces in the alert roll-up. + // Render the M0V3 affordances inside the selection bar — separate + // helper because the picker variant needs a select + button pair, + // not the single-button shape addBulkButton ships. Backend + // `topology::set_parent` refuses moves it can't satisfy (manager + // included) and the refusal surfaces in the bulk-action error + // roll-up. function addMoveActions(parent, selected, containers) { + // 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', 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: !someNotAtRoot + ? '⇡ M0V3 → ROOT not available — all selected agents are already at root' + : null, + }); + + // M0V3 → : inline `