Compare commits
2 changed files with 1 additions and 215 deletions
|
|
@ -316,82 +316,6 @@ a:hover {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
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
|
/* Pending state splits queued vs running — queued ops show only
|
||||||
the pending-state badge (no row tint), running ops keep the
|
the pending-state badge (no row tint), running ops keep the
|
||||||
amber row tint AND get a rotating amber ring on the icon. See
|
amber row tint AND get a rotating amber ring on the icon. See
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Re-derive port conflicts from the live containers map. Mirrors the
|
||||||
// server-side `build_port_conflicts` so the banner reacts to event
|
// server-side `build_port_conflicts` so the banner reacts to event
|
||||||
// updates instead of waiting for a /api/state refetch.
|
// 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
|
// See docs/web-ui.md::Topology tree for the rendering contract
|
||||||
// (forest walk, alphabetical sort, orphan + cycle handling).
|
// (forest walk, alphabetical sort, orphan + cycle handling).
|
||||||
function buildAgentTree(containers) {
|
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();
|
const byName = new Map();
|
||||||
for (const c of containers) byName.set(c.name, c);
|
for (const c of containers) byName.set(c.name, c);
|
||||||
const children = new Map(); // parent_name → [child_name, …]
|
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.
|
// here since it opens the side panel rather than a link.
|
||||||
body.append(drill);
|
body.append(drill);
|
||||||
|
|
||||||
li.append(icon, body, buildAgentMenu(c));
|
li.append(icon, body);
|
||||||
ul.append(li);
|
ul.append(li);
|
||||||
}
|
}
|
||||||
root.append(ul);
|
root.append(ul);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue