agent: remove rebuild button, move dashboard link into the links menu
mara (#3704): 'remove rebuild button, move link to dashboards into links menu.' The overflow (⋯) menu existed for exactly two items: the dashboard back-link and a rebuild-container action. Rebuild is gone outright — the dashboard's own R3BU1LD button already covers it, this was just a rarely-used shortcut not worth its own menu. The dashboard link moves into MetaNav's links popover (now the first item, above stats/forge/ config/extras) instead. With both gone, OverflowMenu had nothing left to justify existing as a separate component — deleted along with its CSS and the now-unused rebuildAction.ts (only consumer). MetaNav gained a dashboardBase prop (Root.tsx already computes this via resolveDashboardBase for InboxPanel/pause — reused, not duplicated) and renders the dashboard link as a real <a>, same treatment as every other item in that popover — no dangling window.open()-only affordance. Updated docs/web-ui/agent.md's Header section and the couple of now-stale OverflowMenu references in index.html's/MetaNav.css's own comments. Verified: header now shows a single trailing icon-badge (was two), popover opens with dashboard first then the agent_links() set. tsc --noEmit clean, build clean, both pre-push lints clean.
This commit is contained in:
parent
8e97fc6263
commit
668ccc2278
8 changed files with 71 additions and 143 deletions
|
|
@ -69,29 +69,34 @@ structurally rather than for one specific trigger. Two columns:
|
||||||
unprivileged process can't make directly).
|
unprivileged process can't make directly).
|
||||||
- **Inbox / todos pills** (`📬 inbox · N` / `📋 todos · N`): hidden
|
- **Inbox / todos pills** (`📬 inbox · N` / `📋 todos · N`): hidden
|
||||||
when empty; click opens the matching flyout in the side panel.
|
when empty; click opens the matching flyout in the side panel.
|
||||||
- **Links badge** (`🔗`): opens a popover listing this agent's
|
- **Links badge** (`🔗`): opens a popover listing `↑ dashboard`
|
||||||
`agent_links()`-sourced navigation (stats, screen when VNC is
|
(back-link to the host dashboard, `${dashboardBase}dashboard.html`)
|
||||||
enabled, forge profile + config-repo mirror when the agent has a
|
followed by this agent's `agent_links()`-sourced navigation (stats,
|
||||||
forge account, any `hyperhive.dashboardLinks` extras) as real
|
screen when VNC is enabled, forge profile + config-repo mirror when
|
||||||
`<a>` elements — not a `Dropdown`-style command list, so
|
the agent has a forge account, any `hyperhive.dashboardLinks`
|
||||||
ctrl/middle-click and "copy link address" keep working. Each
|
extras) — all as real `<a>` elements, not a `Dropdown`-style
|
||||||
`AgentLink.kind` resolves differently: `container` → same-origin
|
command list, so ctrl/middle-click and "copy link address" keep
|
||||||
path; `forge` → `state.forge_public_url + url`, and the link is
|
working. Each `AgentLink.kind` resolves differently: `container` →
|
||||||
dropped entirely when that's unset (never guessed from
|
same-origin path; `forge` → `state.forge_public_url + url`, and the
|
||||||
|
link is dropped entirely when that's unset (never guessed from
|
||||||
`<host>:3000`); `external` → already absolute. Same source
|
`<host>:3000`); `external` → already absolute. Same source
|
||||||
(`GET /api/state`'s `links` field) also feeds
|
(`GET /api/state`'s `links` field) also feeds
|
||||||
`DashboardState.links` for the dashboard card's icon strip —
|
`DashboardState.links` for the dashboard card's icon strip —
|
||||||
`agent_links()` in hive-agent is the single source of truth for
|
`agent_links()` in hive-agent is the single source of truth for
|
||||||
both.
|
both. There is no separate overflow (`⋯`) menu any more — it used
|
||||||
- **Overflow badge** (`⋯`): a `Dropdown` with two rows —
|
to hold exactly this dashboard link plus a rebuild-container action
|
||||||
`↑ dashboard` (link) and `↻ rebuild container` (select-twice
|
(mara, hyperhive#3704: "remove rebuild button, move link to
|
||||||
confirm, same action as the dashboard R3BU1LD button). Everything
|
dashboards into links menu") — rebuild had no real discoverability
|
||||||
else that used to live here (model/effort pickers, new-session,
|
need of its own (the dashboard's own R3BU1LD button already covers
|
||||||
logout) has a better home now: pickers are real badges above, and
|
it) so it's gone outright, and the dashboard link moved here,
|
||||||
`/new-session` / `/logout` are typed slash commands with their own
|
leaving nothing to justify a separate menu. Everything else that
|
||||||
type-twice confirm (see below) — a modal doesn't fit a text-input
|
used to live in the old overflow menu (model/effort pickers,
|
||||||
flow, and burying rare-but-important actions in one flat menu was
|
new-session, logout) already had a better home before this: pickers
|
||||||
the design guide's own named anti-example.
|
are real badges above, and `/new-session` / `/logout` are typed
|
||||||
|
slash commands with their own type-twice confirm (see below) — a
|
||||||
|
modal doesn't fit a text-input flow, and burying rare-but-important
|
||||||
|
actions in one flat menu was the design guide's own named
|
||||||
|
anti-example.
|
||||||
- No header cancel-turn button any more — `/cancel` (slash command,
|
- No header cancel-turn button any more — `/cancel` (slash command,
|
||||||
below) is the only path; the turn-loop state badge already shows
|
below) is the only path; the turn-loop state badge already shows
|
||||||
`thinking` as the discoverability cue.
|
`thinking` as the discoverability cue.
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import { SidePanel } from './components/SidePanel.js';
|
||||||
import { InboxPanel } from './components/InboxPanel.js';
|
import { InboxPanel } from './components/InboxPanel.js';
|
||||||
import { TodosPanel } from './components/TodosPanel.js';
|
import { TodosPanel } from './components/TodosPanel.js';
|
||||||
import { TermInput } from './components/TermInput.js';
|
import { TermInput } from './components/TermInput.js';
|
||||||
import { OverflowMenu } from './components/OverflowMenu.js';
|
|
||||||
import { LoginFlow } from './components/LoginFlow.js';
|
import { LoginFlow } from './components/LoginFlow.js';
|
||||||
import { useAgentState } from './hooks/useAgentState.js';
|
import { useAgentState } from './hooks/useAgentState.js';
|
||||||
import { useTodos } from './hooks/useTodos.js';
|
import { useTodos } from './hooks/useTodos.js';
|
||||||
|
|
@ -89,11 +88,18 @@ export function Root() {
|
||||||
<>
|
<>
|
||||||
<HeaderPill kind="inbox" icon="📬" label="inbox" count={state?.inbox.length ?? 0} onClick={() => setOpenPanel('inbox')} />
|
<HeaderPill kind="inbox" icon="📬" label="inbox" count={state?.inbox.length ?? 0} onClick={() => setOpenPanel('inbox')} />
|
||||||
<HeaderPill kind="todos" icon="📋" label="todos" count={todos.length} onClick={() => setOpenPanel('todos')} />
|
<HeaderPill kind="todos" icon="📋" label="todos" count={todos.length} onClick={() => setOpenPanel('todos')} />
|
||||||
{/* Needs a loaded snapshot for their data (links/forge_public_url,
|
{/* Needs a loaded snapshot for its data (links/forge_public_url,
|
||||||
* dashboard_port) — both omitted (like the old page's
|
* dashboard_port) — omitted until the first /api/state resolves,
|
||||||
* populateOverflowMenu) until the first /api/state resolves. */}
|
* same as the old page's populateOverflowMenu (now deleted —
|
||||||
{state ? <MetaNav links={state.links} forgePublicUrl={state.forge_public_url} /> : null}
|
* dashboard-link moved here, rebuild removed outright, see
|
||||||
{state ? <OverflowMenu label={state.label} dashboardBase={resolveDashboardBase(state.dashboard_port)} /> : null}
|
* MetaNav.tsx's file comment). */}
|
||||||
|
{state ? (
|
||||||
|
<MetaNav
|
||||||
|
links={state.links}
|
||||||
|
forgePublicUrl={state.forge_public_url}
|
||||||
|
dashboardBase={resolveDashboardBase(state.dashboard_port)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
// Kept mounted regardless of `openPanel` (open/closed toggles just the
|
// Kept mounted regardless of `openPanel` (open/closed toggles just the
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,8 @@
|
||||||
same "reuse the values, not the component" call `.login-card`
|
same "reuse the values, not the component" call `.login-card`
|
||||||
(LoginFlow.css) already made, for the same reason: this popover's
|
(LoginFlow.css) already made, for the same reason: this popover's
|
||||||
items are real `<a>` tags, not `Dropdown`'s command-dispatch
|
items are real `<a>` tags, not `Dropdown`'s command-dispatch
|
||||||
`<button>`s. Right-anchored, same as OverflowMenu.css's
|
`<button>`s. Right-anchored — MetaNav sits in the header's right-hand
|
||||||
`.header-overflow-anchor` — MetaNav sits in the same right-hand
|
pills cluster, so a left anchor would push it off-screen. */
|
||||||
pills cluster, so a left anchor would push it off-screen the same
|
|
||||||
way. */
|
|
||||||
.meta-nav-anchor {
|
.meta-nav-anchor {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,27 @@
|
||||||
// ("🔗") in the pills cluster that opens a popover listing
|
// ("🔗") in the pills cluster that opens a popover listing
|
||||||
// stats/screen/forge/config + any `hyperhive.dashboardLinks` extras,
|
// stats/screen/forge/config + any `hyperhive.dashboardLinks` extras,
|
||||||
// sourced from the backend's `agent_links()` (the single source of
|
// sourced from the backend's `agent_links()` (the single source of
|
||||||
// truth — same list also feeds the dashboard card's icon strip).
|
// truth — same list also feeds the dashboard card's icon strip) — plus
|
||||||
//
|
// a `↑ dashboard` back-link, formerly the overflow menu's job (mara:
|
||||||
|
// "remove rebuild button, move link to dashboards into links menu" —
|
||||||
|
// the overflow `⋯` trigger existed for exactly two items, dashboard-
|
||||||
|
// link and rebuild; rebuild's gone outright, the dashboard's own
|
||||||
|
// R3BU1LD button already covers it, dashboard-link moves here, so
|
||||||
|
// `OverflowMenu` had nothing left to justify existing and is deleted).
|
||||||
|
|
||||||
// v1 of this rendered every link inline in the header's title row —
|
// v1 of this rendered every link inline in the header's title row —
|
||||||
// mara, live: "the login card looks like it is behind the header"
|
// mara, live: "the login card looks like it is behind the header"
|
||||||
// (a variable-width link list can grow the title row past the fixed
|
// (a variable-width link list can grow the title row past the fixed
|
||||||
// `--agent-header-h` the rest of the page is offset against — see
|
// `--agent-header-h` the rest of the page is offset against — see
|
||||||
// Header.tsx's comment) and "the links should have the same popout as
|
// Header.tsx's comment) and "the links should have the same popout as
|
||||||
// the links in the nav bar of swarm-ui." This is that: same shape as
|
// the links in the nav bar of swarm-ui." This is that: one quiet icon
|
||||||
// swarm-ui's own `LinksMenu` (Shell/LinksMenu.tsx) — one quiet icon
|
// trigger (reuses `@hive/shared`'s `Badge`, icon-only), a popover on
|
||||||
// trigger, a popover on click, real `<a>` items (not a command-dispatch
|
// click, real `<a>` items — not `@hive/shared`'s `Dropdown` (its items
|
||||||
// button list) so ctrl/middle-click and "copy link address" keep
|
// are always `<button>`s for command dispatch, which would lose real
|
||||||
// working. Trigger itself reuses `@hive/shared`'s `Badge` (icon-only,
|
// link semantics like ctrl/middle-click and "copy link address") —
|
||||||
// same shape as the header's `⋯` overflow trigger — same touch target,
|
// but matching its `.ui-dropdown` visual values exactly (see
|
||||||
// same hover/expanded treatment, one visual language for every header
|
// MetaNav.css), the same "reuse the values, not the component" call
|
||||||
// trigger) rather than a bespoke button. The popover isn't built from
|
// `LoginFlow`'s `.login-card` already made for the same reason.
|
||||||
// `@hive/shared`'s `Dropdown` — its items are always `<button>`s
|
|
||||||
// (command dispatch), which would lose real link semantics — but
|
|
||||||
// matches its `.ui-dropdown` visual values exactly (see MetaNav.css),
|
|
||||||
// the same "reuse the values, not the component" call `LoginFlow`'s
|
|
||||||
// `.login-card` already made for the same reason.
|
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { Badge } from '@hive/shared/badge.js';
|
import { Badge } from '@hive/shared/badge.js';
|
||||||
import type { AgentLink } from '../types.js';
|
import type { AgentLink } from '../types.js';
|
||||||
|
|
@ -30,9 +31,12 @@ import './MetaNav.css';
|
||||||
export interface MetaNavProps {
|
export interface MetaNavProps {
|
||||||
links: AgentLink[];
|
links: AgentLink[];
|
||||||
forgePublicUrl: string | null;
|
forgePublicUrl: string | null;
|
||||||
|
/** Absolute base URL of the host dashboard (`resolveDashboardBase`)
|
||||||
|
* — the `↑ dashboard` item links to `${dashboardBase}dashboard.html`. */
|
||||||
|
dashboardBase: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MetaNav({ links, forgePublicUrl }: MetaNavProps) {
|
export function MetaNav({ links, forgePublicUrl, dashboardBase }: MetaNavProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
@ -58,13 +62,24 @@ export function MetaNav({ links, forgePublicUrl }: MetaNavProps) {
|
||||||
// entirely (never guessed from `<host>:3000`); `external` is already
|
// entirely (never guessed from `<host>:3000`); `external` is already
|
||||||
// absolute; `container` is a same-origin path.
|
// absolute; `container` is a same-origin path.
|
||||||
const visible = links.filter((lnk) => lnk.kind !== 'forge' || forgePublicUrl);
|
const visible = links.filter((lnk) => lnk.kind !== 'forge' || forgePublicUrl);
|
||||||
if (visible.length === 0) return null;
|
// No early-return-on-empty any more — the dashboard link below is
|
||||||
|
// always present, so the trigger always has at least one item.
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="meta-nav-anchor" ref={rootRef}>
|
<div class="meta-nav-anchor" ref={rootRef}>
|
||||||
<Badge value="🔗" title="agent links" onClick={() => setOpen((o) => !o)} expanded={open} />
|
<Badge value="🔗" title="agent links" onClick={() => setOpen((o) => !o)} expanded={open} />
|
||||||
{open ? (
|
{open ? (
|
||||||
<div class="meta-nav-popover" role="menu" aria-label="agent links">
|
<div class="meta-nav-popover" role="menu" aria-label="agent links">
|
||||||
|
<a
|
||||||
|
class="meta-nav-item"
|
||||||
|
href={`${dashboardBase}dashboard.html`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
↑ dashboard
|
||||||
|
</a>
|
||||||
{visible.map((lnk) => {
|
{visible.map((lnk) => {
|
||||||
const href = lnk.kind === 'forge' ? `${forgePublicUrl}${lnk.url}` : lnk.url;
|
const href = lnk.kind === 'forge' ? `${forgePublicUrl}${lnk.url}` : lnk.url;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
/* Anchors <Dropdown> under the "⋯" trigger — same positioning
|
|
||||||
contract as StatusChips.css's `.status-chip-anchor`, kept as its own
|
|
||||||
rule here rather than reused across components (see this file's
|
|
||||||
sibling .tsx comment) so OverflowMenu doesn't depend on StatusChips'
|
|
||||||
incidental CSS. */
|
|
||||||
.header-overflow-anchor {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
/* @hive/shared's Dropdown.css anchors left-edge-to-left-edge, correct
|
|
||||||
for a picker with room to its right (StatusChips' model/effort). The
|
|
||||||
overflow trigger is the header's right-most element instead, so a
|
|
||||||
left anchor pushes the menu off-screen — flip to right-edge-to-
|
|
||||||
right-edge here, scoped to this one consumer rather than changing
|
|
||||||
the shared default (which is still correct for every other caller). */
|
|
||||||
.header-overflow-anchor .ui-dropdown {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
// <OverflowMenu> — the header's `⋯` trigger. Deliberately trimmed vs.
|
|
||||||
// app.js's `populateOverflowMenu`: that one held dashboard-link/rebuild/
|
|
||||||
// new-session/logout/model-picker/effort-picker all in one flat list —
|
|
||||||
// the design guide's own named anti-example for a junk drawer. The
|
|
||||||
// model/effort pickers already moved to StatusChips' own Badge+Dropdown
|
|
||||||
// controls (this rewrite's first commit); new-session/logout now live
|
|
||||||
// as TermInput slash commands (typed, not menu-clicked, and already
|
|
||||||
// have their own two-step confirm). What's left here — rebuild + a
|
|
||||||
// dashboard back-link — are genuinely menu-shaped (rare, not tied to
|
|
||||||
// any other visible control), so they're still a `⋯` menu, just a much
|
|
||||||
// shorter one.
|
|
||||||
//
|
|
||||||
// Reuses `@hive/shared`'s `Badge`+`Dropdown` — same anchored-popover
|
|
||||||
// pair StatusChips' model/effort pickers use, not a bespoke popover.
|
|
||||||
// Rebuild's confirm is the same "select once to arm, select again to
|
|
||||||
// fire" pattern as TermInput's `/new-session`/`/logout` — adapted here
|
|
||||||
// to a menu click instead of a typed repeat, but the same "requires
|
|
||||||
// deliberate repetition, not a modal" idea.
|
|
||||||
import { useState } from 'preact/hooks';
|
|
||||||
import { Badge } from '@hive/shared/badge.js';
|
|
||||||
import { Dropdown, type DropdownOption } from '@hive/shared/dropdown.js';
|
|
||||||
import { submitRebuild } from '../lib/rebuildAction.js';
|
|
||||||
import './OverflowMenu.css';
|
|
||||||
|
|
||||||
export interface OverflowMenuProps {
|
|
||||||
label: string;
|
|
||||||
dashboardBase: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OverflowMenu({ label, dashboardBase }: OverflowMenuProps) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [armed, setArmed] = useState(false);
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
setOpen(false);
|
|
||||||
setArmed(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: DropdownOption[] = [
|
|
||||||
{ value: 'dashboard', label: '↑ dashboard' },
|
|
||||||
{ value: 'rebuild', label: armed ? '↻ rebuild — click again to confirm' : '↻ rebuild container' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function onSelect(value: string) {
|
|
||||||
if (value === 'dashboard') {
|
|
||||||
window.open(`${dashboardBase}dashboard.html`, '_blank', 'noopener');
|
|
||||||
close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value === 'rebuild') {
|
|
||||||
if (armed) {
|
|
||||||
submitRebuild(dashboardBase, label);
|
|
||||||
close();
|
|
||||||
} else {
|
|
||||||
setArmed(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="header-overflow-anchor">
|
|
||||||
<Badge value="⋯" title="more actions" onClick={() => setOpen((o) => !o)} expanded={open} />
|
|
||||||
<Dropdown open={open} options={options} label="more actions" onSelect={onSelect} onClose={close} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<link rel="stylesheet" href="static/theme.css">
|
<link rel="stylesheet" href="static/theme.css">
|
||||||
<link rel="stylesheet" href="static/agent.css">
|
<link rel="stylesheet" href="static/agent.css">
|
||||||
<!-- main.css (bundled component CSS: Badge/Dropdown/LoginFlow/MetaNav/
|
<!-- main.css (bundled component CSS: Badge/Dropdown/LoginFlow/MetaNav/
|
||||||
StatusChips/OverflowMenu/SidePanel/...) MUST load after agent.css —
|
StatusChips/SidePanel/...) MUST load after agent.css —
|
||||||
`.login-card`/`.meta-nav-popover`/etc. share class-selector
|
`.login-card`/`.meta-nav-popover`/etc. share class-selector
|
||||||
specificity with agent.css's legacy rules, so load order is what
|
specificity with agent.css's legacy rules, so load order is what
|
||||||
decides which one wins the cascade (see LoginFlow.css/MetaNav.css
|
decides which one wins the cascade (see LoginFlow.css/MetaNav.css
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
// Rebuild POST to the *dashboard's* origin — same cross-origin-needs-a-
|
|
||||||
// real-form-submit reasoning as pauseAction.ts's `submitPauseResume`
|
|
||||||
// (hive-c0re's `/api/rebuild/{name}` returns a plain 200, no CORS
|
|
||||||
// header, so a cross-origin `fetch` can't read the result).
|
|
||||||
export function submitRebuild(dashboardBase: string, label: string): void {
|
|
||||||
const form = document.createElement('form');
|
|
||||||
form.method = 'POST';
|
|
||||||
form.action = `${dashboardBase}api/rebuild/${label}`;
|
|
||||||
document.body.appendChild(form);
|
|
||||||
form.submit();
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue