diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 29f59830..04545a12 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -316,82 +316,6 @@ a:hover { flex: 1; min-width: 0; } - -/* ── per-agent three-dot context menu ───────────────────────────────────── - Positioned after .card-body in the flex row. The ⋮ button is invisible - until the row is hovered (or the menu is open) so quiet rows stay clean. - The dropdown is absolute-positioned relative to .agent-menu and opens - below + right-aligned to the button. */ -.agent-menu { - flex: none; - position: relative; - align-self: flex-start; - margin-top: 0.3em; -} -.agent-menu-btn { - display: block; - background: none; - border: none; - color: var(--subtext0); - font-size: 1.1em; - line-height: 1; - cursor: pointer; - padding: 0.1em 0.4em; - border-radius: 4px; - opacity: 0; - transition: opacity 120ms, background 120ms, color 120ms; -} -/* Show on row hover or when the menu is open. */ -.container-row:hover .agent-menu-btn, -.agent-menu.open .agent-menu-btn { - opacity: 1; -} -.agent-menu-btn:hover, -.agent-menu-btn:focus-visible { - background: var(--surface1); - color: var(--text); - opacity: 1; -} -.agent-menu-btn:focus-visible { - outline: 2px solid var(--purple); - outline-offset: 1px; -} -.agent-menu-dropdown { - position: absolute; - right: 0; - top: calc(100% + 2px); - z-index: 200; - background: var(--surface0); - border: 1px solid var(--surface2); - border-radius: 6px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45); - list-style: none; - margin: 0; - padding: 0.3em 0; - min-width: 10em; - white-space: nowrap; -} -.agent-menu-item { - display: block; - width: 100%; - background: none; - border: none; - color: var(--text); - font-family: inherit; - font-size: 0.82em; - letter-spacing: 0.01em; - text-align: left; - padding: 0.4em 0.9em; - cursor: pointer; -} -.agent-menu-item:hover { - background: var(--surface1); -} -.agent-menu-sep { - height: 1px; - background: var(--surface2); - margin: 0.3em 0; -} /* Pending state splits queued vs running — queued ops show only the pending-state badge (no row tint), running ops keep the amber row tint AND get a rotating amber ring on the icon. See diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 2a6229b7..38a20e39 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -325,141 +325,6 @@ window.marked = marked; } }); - // ─── per-agent context menu ────────────────────────────────────────── - // Three-dot (⋮) button on each agent card for quick single-agent - // lifecycle actions without needing to select first. State-aware: - // restart/stop only shown when running, start only shown when stopped, - // destroy/purge hidden for the manager. - // The button is CSS-invisible until the row is hovered (or menu is - // open) so it doesn't clutter quiet rows. - - let openAgentMenu = null; // currently open dropdown element, or null - - function closeAllAgentMenus() { - if (!openAgentMenu) return; - openAgentMenu.hidden = true; - const wrap = openAgentMenu.closest('.agent-menu'); - if (wrap) { - wrap.classList.remove('open'); - const btn = wrap.querySelector('.agent-menu-btn'); - if (btn) btn.setAttribute('aria-expanded', 'false'); - } - openAgentMenu = null; - } - - // Close on any click outside an agent-menu element. - document.addEventListener('click', (e) => { - if (!e.target.closest('.agent-menu')) closeAllAgentMenus(); - }, true); - // Close on Escape. stopImmediatePropagation so the selection-clear - // handler on the same element doesn't also fire when a menu is open. - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && openAgentMenu) { - closeAllAgentMenus(); - e.stopImmediatePropagation(); - } - }, true); - - // Single-agent POST helper shared by all menu items. - async function agentMenuPost(actionPath, name, body) { - const url = actionPath + encodeURIComponent(name); - try { - const resp = await fetch(url, { - method: 'POST', - headers: body ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {}, - body: body ? new URLSearchParams(body) : undefined, - redirect: 'manual', - }); - const ok = resp.ok || resp.type === 'opaqueredirect' - || (resp.status >= 200 && resp.status < 400); - if (!ok) { - const text = await resp.text().catch(() => ''); - alert('action failed: ' + resp.status + (text ? '\n\n' + text : '')); - } - } catch (err) { - alert('action failed: ' + err); - } - } - - function buildAgentMenu(c) { - const wrap = el('div', { class: 'agent-menu' }); - const btn = el('button', { - type: 'button', - class: 'agent-menu-btn', - title: `actions for ${c.name}`, - 'aria-label': `actions for ${c.name}`, - 'aria-haspopup': 'menu', - 'aria-expanded': 'false', - }, '⋮'); - - const dropdown = el('ul', { class: 'agent-menu-dropdown', hidden: true, role: 'menu' }); - - function menuItem(label, opts) { - const li = el('li', { role: 'presentation' }); - const item = el('button', { - type: 'button', - class: 'agent-menu-item', - role: 'menuitem', - }, label); - item.addEventListener('click', async () => { - closeAllAgentMenus(); - if (opts.confirm && !confirm(opts.confirm)) return; - await agentMenuPost(opts.action, c.name, opts.body || null); - }); - li.append(item); - return li; - } - - function menuSep() { - return el('li', { class: 'agent-menu-sep', role: 'separator' }); - } - - // Show only actions that are applicable in the current state. - if (c.running) { - dropdown.append( - menuItem('↺ R3ST4RT', { action: '/restart/', confirm: `restart ${c.name}?` }), - menuItem('■ ST0P', { action: '/kill/', confirm: `stop ${c.name}?` }), - ); - } else { - dropdown.append( - menuItem('▶ ST4RT', { action: '/start/', confirm: `start ${c.name}?` }), - ); - } - dropdown.append( - menuItem('↻ R3BU1LD', { action: '/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }), - ); - - if (!c.is_manager) { - dropdown.append( - menuSep(), - menuItem('DESTR0Y', { - action: '/destroy/', - confirm: `destroy ${c.name}? container removed; state + creds kept.`, - }), - menuItem('PURG3', { - action: '/destroy/', - body: { purge: 'on' }, - confirm: `PURGE ${c.name}? WIPES container, config history, claude creds, and notes. no undo.`, - }), - ); - } - - btn.addEventListener('click', (e) => { - e.stopPropagation(); - const wasHidden = dropdown.hidden; - closeAllAgentMenus(); - if (wasHidden) { - dropdown.hidden = false; - btn.setAttribute('aria-expanded', 'true'); - wrap.classList.add('open'); - openAgentMenu = dropdown; - } - }); - - wrap.append(btn, dropdown); - return wrap; - } - // Re-derive port conflicts from the live containers map. Mirrors the // server-side `build_port_conflicts` so the banner reacts to event // updates instead of waiting for a /api/state refetch. @@ -485,9 +350,6 @@ window.marked = marked; // See docs/web-ui.md::Topology tree for the rendering contract // (forest walk, alphabetical sort, orphan + cycle handling). function buildAgentTree(containers) { - // Close any open context menu before replacing the DOM tree — the - // previous dropdown element would otherwise be a stale reference. - closeAllAgentMenus(); const byName = new Map(); for (const c of containers) byName.set(c.name, c); const children = new Map(); // parent_name → [child_name, …] @@ -859,7 +721,7 @@ window.marked = marked; // here since it opens the side panel rather than a link. body.append(drill); - li.append(icon, body, buildAgentMenu(c)); + li.append(icon, body); ul.append(li); } root.append(ul);