diff --git a/frontend/packages/dashboard/src/agent-menu/hive-agent-menu.js b/frontend/packages/dashboard/src/agent-menu/hive-agent-menu.js index 54fe3b60..bfed9242 100644 --- a/frontend/packages/dashboard/src/agent-menu/hive-agent-menu.js +++ b/frontend/packages/dashboard/src/agent-menu/hive-agent-menu.js @@ -49,6 +49,13 @@ async function agentMenuPost(actionPath, name, body, graceful) { class HiveAgentMenu extends HTMLElement { connectedCallback() { + // Same reconnect-without-detach hazard as (see its + // connectedCallback comment) — swarm.js's row cache can move an + // already-built
  • subtree without a real detach. Without this + // guard a reconnect would append a second on top of the + // first, doubling the dropdown. + if (this._menu) return; + const { c, forgeBase } = this._opts || {}; const btn = el('button', { diff --git a/frontend/packages/shared/src/hive-menu/hive-menu.js b/frontend/packages/shared/src/hive-menu/hive-menu.js index 50d06625..53d8da8b 100644 --- a/frontend/packages/shared/src/hive-menu/hive-menu.js +++ b/frontend/packages/shared/src/hive-menu/hive-menu.js @@ -65,6 +65,16 @@ document.addEventListener('keydown', (e) => { class HiveMenu extends HTMLElement { connectedCallback() { + // A custom element's connectedCallback fires again on a same-document + // *move* (insertBefore/append repositioning an already-connected node + // runs the removal + insertion steps for its whole subtree), not just + // on a fresh mount — swarm.js's row-fingerprint cache reuses/reorders + // existing
  • subtrees, which can move an already-initialised + // without ever detaching it from the document. Guard + // against re-running setup (attachShadow throws if the host already + // has a shadow root) on that reconnect. + if (this.shadowRoot) return; + const { trigger, content } = this._opts || {}; const root = attachShadowCss(this, hiveMenuCss);