frontend: drop the Panel forwarding object and the side-panel-body compat class

Per mara's review: '2 and maybe 1, but 3 also sounds reasonable on first
glance' (against 3 options I posted). Doing 2 and 1, leaving open()/
openNamed() as-is (option 3, tentative only).

Both dashboard/common.js and agent/app.js now export/use the
<hive-side-panel> element instance directly (sidePanel) instead of a
thin Panel = { open, openNamed, refresh, close } object that existed
purely to keep the old call-site shape unchanged. All 6 real call sites
updated to call the element's own methods directly.

The .side-panel-body class each wrapper stamped onto its own instance,
purely so common.css/agent.css's pre-existing content-styling selectors
kept matching, is gone too -- those selectors now use the element's own
tag name as the root (hive-side-panel .md, hive-side-panel .agent-inbox),
which already uniquely identifies the light-DOM instance without a
compatibility class. Verified via headless Chromium/CDP that the
tag-name selectors resolve correctly with no class needed.

Drive-by: removed an unrelated dead Panel import in call.js.
This commit is contained in:
iris 2026-08-01 00:46:02 +02:00 committed by mara
commit 399a837e17
8 changed files with 73 additions and 90 deletions

View file

@ -901,10 +901,10 @@ pre.diff {
element's own shadow tree) and the styling for the panel's own
content types, which the shared element's shadow tree can never
reach (see @hive/shared/side-panel/hive-side-panel.css's header
comment). `.side-panel-body` below is the compatibility class
app.js's `Panel` wrapper puts directly on its `<hive-side-panel>`
instance so these rules keep reaching the slotted content by
ordinary light-DOM descendant matching. */
comment). The `hive-side-panel .agent-inbox ` rules below use the
element's own tag name as the selector root no compatibility class
needed, the tag name already uniquely identifies the light-DOM
instance app.js's `sidePanel` creates. */
body.side-panel-resizing {
user-select: none;
}
@ -913,12 +913,12 @@ body.side-panel-resizing * { cursor: ew-resize !important; }
legacy <details>-collapsible variant of .agent-inbox is gone, so
here we strip the inbox-only chrome (background, border-left) and
let the panel body's own padding own the framing. */
.side-panel-body .agent-inbox {
hive-side-panel .agent-inbox {
margin: 0;
font-size: inherit;
color: var(--fg);
}
.side-panel-body .agent-inbox ul {
hive-side-panel .agent-inbox ul {
background: transparent;
border-left: 0;
padding: 0;

View file

@ -38,26 +38,17 @@ window.marked = marked;
bindAsyncForms(() => refreshState());
// ─── side panel (singleton drawer for inbox + loose-ends flyouts) ──────
// Thin wrapper around the shared `<hive-side-panel>` element (see
// @hive/shared/side-panel.js) — created once, eagerly, when this
// The shared `<hive-side-panel>` element (see @hive/shared/side-panel.js
// for the chrome/behavior it owns), created once, eagerly, when this
// module's IIFE runs (ES modules execute after the document is
// parsed, so `document.body` already exists; no per-call lazy-init
// guard to remember for any method, present or future). This UI's
// `open(name, title, content)` always takes an owner name, so it maps
// straight onto the shared element's `openNamed`; `side-panel-body`
// is a plain compatibility class this wrapper puts on its own
// instance so `agent.css`'s existing `.side-panel-body .agent-inbox
// …` rules keep reaching the slotted content by ordinary light-DOM
// descendant matching.
const panelEl = document.createElement('hive-side-panel');
panelEl.classList.add('side-panel-body');
document.body.append(panelEl);
const Panel = {
open: (name, title, content) => panelEl.openNamed(name, title, content),
close: () => panelEl.close(),
refresh: (name, title, content) => panelEl.refresh(name, title, content),
currentOwner: () => panelEl.currentOwner(),
};
// parsed, so `document.body` already exists). Used directly — this
// UI's opens always take an owner name, so call sites use the
// element's own `openNamed(name, title, content)` rather than the
// untyped `open`. `agent.css` reaches the slotted content via a plain
// `hive-side-panel .agent-inbox …` tag-name selector (no compat class
// needed — the element's own tag name already identifies it).
const sidePanel = document.createElement('hive-side-panel');
document.body.append(sidePanel);
// Wire the header pills to open the side panel. Pre-built (vs
// re-building per-click) so the freshest snapshot already lives
@ -67,14 +58,14 @@ window.marked = marked;
const inboxPill = $('inbox-pill');
if (inboxPill) {
inboxPill.addEventListener('click', () => {
Panel.open('inbox', 'inbox · ' + lastInbox.length,
sidePanel.openNamed('inbox', 'inbox · ' + lastInbox.length,
buildInboxList(lastInbox));
});
}
const todosPill = $('todos-pill');
if (todosPill) {
todosPill.addEventListener('click', () => {
Panel.open('todos', 'todos · ' + lastTodos.length,
sidePanel.openNamed('todos', 'todos · ' + lastTodos.length,
buildTodosList(lastTodos));
});
}
@ -336,7 +327,7 @@ window.marked = marked;
else closeOverflowMenu();
}
// Wire once on boot. The trigger itself + click-outside + Escape
// dismissal pattern matches the side-panel flyout (Panel).
// dismissal pattern matches the side-panel flyout (sidePanel).
(function bindOverflowMenu() {
const btn = $('overflow-btn');
if (!btn) return;
@ -807,7 +798,7 @@ window.marked = marked;
function buildLooseEndsList(threads) {
// Returns the <div> the side panel renders. The structural shape
// mirrors the legacy <details>-collapsible block — same CSS rules
// apply via `.side-panel-body .agent-inbox`.
// apply via `hive-side-panel .agent-inbox`.
const wrap = el('div', { class: 'agent-inbox' });
if (!threads.length) {
wrap.append(el('p', { class: 'side-panel-empty' },
@ -907,7 +898,7 @@ window.marked = marked;
const count = $('todos-count');
if (count) count.textContent = todos.length;
if (pill) pill.hidden = todos.length === 0;
Panel.refresh('todos', 'todos · ' + todos.length,
sidePanel.refresh('todos', 'todos · ' + todos.length,
buildTodosList(todos));
}
@ -1073,7 +1064,7 @@ window.marked = marked;
const count = $('inbox-count');
if (count) count.textContent = rows.length;
if (pill) pill.hidden = rows.length === 0;
Panel.refresh('inbox', 'inbox · ' + rows.length, buildInboxList(rows));
sidePanel.refresh('inbox', 'inbox · ' + rows.length, buildInboxList(rows));
}
// Harness reachability badge: derived from the same `s.status` the
// status block reads. Each status maps to a glyph + label + colour

View file

@ -14,7 +14,7 @@
// live-mutation paths call an injected `onCountsChanged` callback the entry
// registers once via `initCall`.
import { $, form, Panel, appendLinkified } from './common.js';
import { $, form, appendLinkified } from './common.js';
import { el } from '@hive/shared/dom.js';
import { themedToast } from '@hive/shared/modal.js';
import { epochSec, fmtAgo, fmtDuration } from './util.js';

View file

@ -376,49 +376,49 @@ code {
not just the shared element's own shadow tree) and the styling for
the panel's own content types, which the shared element's shadow tree
can never reach (see @hive/shared/side-panel/hive-side-panel.css's
header comment). `.side-panel-body` below is the compatibility class
common.js's `Panel` wrapper puts directly on its `<hive-side-panel>`
instance so these rules keep reaching the slotted content by ordinary
light-DOM descendant matching. */
header comment). The `hive-side-panel .md ` rules below use the
element's own tag name as the selector root no compatibility class
needed, the tag name already uniquely identifies the light-DOM
instance `common.js`'s `sidePanel` creates. */
body.side-panel-resizing {
user-select: none;
}
body.side-panel-resizing * { cursor: ew-resize !important; }
.side-panel-body .md { color: var(--fg); line-height: 1.5; }
.side-panel-body .md > :first-child { margin-top: 0; }
.side-panel-body .md > :last-child { margin-bottom: 0; }
.side-panel-body .md p { margin: 0.5em 0; }
.side-panel-body .md h1,
.side-panel-body .md h2,
.side-panel-body .md h3,
.side-panel-body .md h4 { color: var(--purple); margin: 0.9em 0 0.4em; }
.side-panel-body .md code {
hive-side-panel .md { color: var(--fg); line-height: 1.5; }
hive-side-panel .md > :first-child { margin-top: 0; }
hive-side-panel .md > :last-child { margin-bottom: 0; }
hive-side-panel .md p { margin: 0.5em 0; }
hive-side-panel .md h1,
hive-side-panel .md h2,
hive-side-panel .md h3,
hive-side-panel .md h4 { color: var(--purple); margin: 0.9em 0 0.4em; }
hive-side-panel .md code {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 2px;
padding: 0.1em 0.3em;
font-size: 0.9em;
}
.side-panel-body .md pre {
hive-side-panel .md pre {
background: var(--crust);
border: 1px solid var(--border);
padding: 0.5em 0.7em;
overflow-x: auto;
margin: 0.5em 0;
}
.side-panel-body .md pre code { background: none; border: none; padding: 0; }
.side-panel-body .md a { color: var(--cyan); }
.side-panel-body .md ul,
.side-panel-body .md ol { margin: 0.4em 0; padding-left: 1.5em; }
.side-panel-body .md blockquote {
hive-side-panel .md pre code { background: none; border: none; padding: 0; }
hive-side-panel .md a { color: var(--cyan); }
hive-side-panel .md ul,
hive-side-panel .md ol { margin: 0.4em 0; padding-left: 1.5em; }
hive-side-panel .md blockquote {
border-left: 3px solid var(--purple-dim);
padding-left: 0.8em;
margin: 0.4em 0;
color: var(--muted);
}
.side-panel-body .md table { border-collapse: collapse; margin: 0.5em 0; }
.side-panel-body .md th,
.side-panel-body .md td {
hive-side-panel .md table { border-collapse: collapse; margin: 0.5em 0; }
hive-side-panel .md th,
hive-side-panel .md td {
border: 1px solid var(--border);
padding: 0.2em 0.5em;
}

View file

@ -281,30 +281,21 @@ export function openBuildLogStream(id, pre, { onDone, onError } = {}) {
// ─── side panel ─────────────────────────────────────────────────────────
// Singleton drawer that swipes in from the right. Long content
// (file previews, approval diffs, journald logs, applied config)
// opens here via `Panel.open(title, node)` instead of expanding
// opens here via `sidePanel.open(title, node)` instead of expanding
// inline. Body is swapped on each open; closing just slides out so
// the content stays visible through the transition.
//
// Thin wrapper around the shared `<hive-side-panel>` element (see
// @hive/shared/side-panel.js for the chrome/behavior it owns, including
// drag-to-resize). The instance is created once, eagerly, at module
// evaluation time — ES modules run after the document is parsed (same
// timing `defer` scripts get), so `document.body` already exists here;
// no per-call lazy-init guard needed, and no easy-to-forget step for
// any new method added later. `side-panel-body` is a plain compatibility
// class this wrapper puts on its own instance so `common.css`'s
// existing `.side-panel-body .md …` rules keep reaching the slotted
// content by ordinary light-DOM descendant matching — the shared
// element itself has no idea that class name means anything.
const panelEl = document.createElement('hive-side-panel');
panelEl.classList.add('side-panel-body');
document.body.append(panelEl);
export const Panel = {
open: (title, content) => panelEl.open(title, content),
openNamed: (name, title, content) => panelEl.openNamed(name, title, content),
refresh: (name, title, content) => panelEl.refresh(name, title, content),
close: () => panelEl.close(),
};
// The instance is created once, eagerly, at module evaluation time — ES
// modules run after the document is parsed (same timing `defer` scripts
// get), so `document.body` already exists here. Exported directly (not
// wrapped in a forwarding object) — callers use the element's own
// `open`/`openNamed`/`refresh`/`close`/`currentOwner` methods, see
// @hive/shared/side-panel.js for what they do. `common.css` reaches the
// slotted content via a plain `hive-side-panel .md …` tag-name selector
// (no compatibility class needed — the element's own tag name already
// uniquely identifies it in the light DOM).
export const sidePanel = document.createElement('hive-side-panel');
document.body.append(sidePanel);
// ─── path linkification ─────────────────────────────────────────────────
// Agents constantly drop pointer strings into messages + question
@ -395,14 +386,14 @@ async function openFilePanel(path) {
'(could not load image — it may be missing or over the preview size cap)'));
});
img.src = '/api/state-file?path=' + encodeURIComponent(path);
Panel.open('↳ ' + path, img);
sidePanel.open('↳ ' + path, img);
return;
}
const isMd = /\.(md|markdown)$/i.test(path);
const isSvg = /\.svg$/i.test(path);
const view = el('div');
view.textContent = '(fetching…)';
Panel.open('↳ ' + path, view);
sidePanel.open('↳ ' + path, view);
try {
const text = await fetchStateFile(path);
if (isSvg) {

View file

@ -276,8 +276,8 @@ window.marked = marked;
// NOTIF.bind() (the enable/mute/unmute toggle UI) moved to /settings.html
// with the S3TT1NGS page. The dashboard keeps NOTIF.show() for approval/
// question notifications — it reads the browser permission + localStorage
// mute flag the settings page sets, so no bind() is needed here. Panel
// (backed by <hive-side-panel>) wires its own internals in
// mute flag the settings page sets, so no bind() is needed here. The
// side panel (<hive-side-panel>) wires its own internals in
// connectedCallback — no bind() step needed either.
// ─── live updates: dashboard event stream ──────────────────────────────

View file

@ -9,11 +9,11 @@
it is slotted, so this file needs zero `::slotted()`. The one slotted
node (the caller's opaque content, passed to open()/openNamed()/
refresh()) is intentionally un-styled from in here: each package's own
content-type-specific rules (`.side-panel-body .md` in the dashboard,
`.side-panel-body .agent-inbox` in the agent UI) live in that package's
own global stylesheet and reach the slotted content via a light-DOM
class its own wrapper puts directly on its `<hive-side-panel>` instance
same architecture floor as `<hive-menu>`'s item-row styling, not a
content-type-specific rules (`hive-side-panel .md` in the dashboard,
`hive-side-panel .agent-inbox` in the agent UI) live in that package's
own global stylesheet and reach the slotted content via the element's
own tag name as the selector root no compatibility class needed,
same architecture floor as `<hive-menu>`'s item-row styling, not a
scope choice.
`:host(.open)`/`:host(.resizing)` respond to the two state classes the

View file

@ -1,10 +1,10 @@
// hive-side-panel.js — <hive-side-panel>, the generic slide-in drawer
// shadow-DOM custom element behind the dashboard's and the per-agent UI's
// side panel (file previews, diffs, logs, inbox/todo lists — anything too
// long to expand inline). One singleton instance per page, created +
// appended lazily by each package's own thin wrapper
// (dashboard/src/common.js's `Panel`, agent/src/app.js's `Panel`) — same
// lazy-creation pattern `themedToast` uses for its toast-stack container.
// long to expand inline). One singleton instance per page, created once,
// eagerly, by each package's own module (dashboard/src/common.js's
// `sidePanel`, agent/src/app.js's `sidePanel`) and used directly — no
// wrapper object, no per-call lazy-init guard.
//
// Builds all structural chrome itself in `connectedCallback` (backdrop,
// drawer, resize handle, header, title, close button) — shadow-owned,
@ -12,8 +12,9 @@
// caller-built DOM, appended as a light-DOM child and picked up by the
// shadow tree's single default `<slot>`, exactly like `<hive-menu>`'s
// `content` (see that file's header for why). Each package's own global
// stylesheet reaches that content the same way it always did, via a
// light-DOM class its own wrapper puts on the instance it owns.
// stylesheet reaches that content via the element's own tag name as the
// selector root (`hive-side-panel .md`, `hive-side-panel .agent-inbox`)
// — no compatibility class needed.
//
// Public API is instance methods: `open(title, content)`,
// `openNamed(name, title, content)`, `refresh(name, title, content)`,