swarm-ui: hives are a folded card on the agents page, and hide developer details also hides the components tab
The hives nav tab and route go, with no redirect; the roster is a card above the agents panel that starts folded and is read on the first open. The settings toggle is renamed from hide debug output to hide developer details, as it now also drops the components tab (the page still answers at /components); the stored preference keeps its key.
This commit is contained in:
parent
0cbfa806d3
commit
70917b7dd0
12 changed files with 110 additions and 49 deletions
|
|
@ -20,21 +20,20 @@ the highlighted one. **Escape**, **Tab** or a click elsewhere shut the list with
|
||||||
changing anything. Typing the start of an option's name picks it, also with the list
|
changing anything. Typing the start of an option's name picks it, also with the list
|
||||||
shut. A dropdown inside a dialog opens above the dialog and the dialog doesn't cut it off.
|
shut. A dropdown inside a dialog opens above the dialog and the dialog doesn't cut it off.
|
||||||
|
|
||||||
| route | what |
|
| route | what |
|
||||||
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `/` | the start page: the swarm's name over a digital-rain backdrop, plus a paperclip offering to set up an agent while no `ruth` exists (it opens the create form with `ruth` filled in); the swarm name in the header links here |
|
| `/` | the start page: the swarm's name over a digital-rain backdrop, plus a paperclip offering to set up an agent while no `ruth` exists (it opens the create form with `ruth` filled in); the swarm name in the header links here |
|
||||||
| `/hives` | the hive directory, each hive with its last reported status |
|
| `/agents` | every agent, under a folded **hives** card with the hive directory and each hive's last reported status: status, config PR, wanted state, inference settings, resource caps, permissions; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
|
||||||
| `/agents` | every agent: status, config PR, wanted state, inference settings, resource caps, permissions; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
|
| `/agents/<name>/terminal` | one agent's live terminal |
|
||||||
| `/agents/<name>/terminal` | one agent's live terminal |
|
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
||||||
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
| `/alerts` | the alerts the swarm's Grafana holds now: the firing ones, with a button for the pending, no-data and error ones and a link to Grafana's own alert list; read through the controller with a Viewer service-account token |
|
||||||
| `/alerts` | the alerts the swarm's Grafana holds now: the firing ones, with a button for the pending, no-data and error ones and a link to Grafana's own alert list; read through the controller with a Viewer service-account token |
|
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
||||||
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
||||||
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
| `/messages` | the swarm-wide message feed: every message between agents, and to them from the operator, reminders and compaction, with a history window and a filter on sender or recipient; a folded **broadcast** card above the feed sends one message to several agents at once |
|
||||||
| `/messages` | the swarm-wide message feed: every message between agents, and to them from the operator, reminders and compaction, with a history window and a filter on sender or recipient; a folded **broadcast** card above the feed sends one message to several agents at once |
|
| `/notifications` | the notification router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows |
|
||||||
| `/notifications` | the notification router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows |
|
| `/issues` | a cross-repo issue report |
|
||||||
| `/issues` | a cross-repo issue report |
|
| `/review` | the review classifier: its recent runs, a try-out on a real PR, and its settings |
|
||||||
| `/review` | the review classifier: its recent runs, a try-out on a real PR, and its settings |
|
| `/builds` | the swarm builder's recent builds of agent config PRs, with what nix printed when one failed |
|
||||||
| `/builds` | the swarm builder's recent builds of agent config PRs, with what nix printed when one failed |
|
|
||||||
|
|
||||||
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
||||||
Pages that refresh on a timer share one polling rule: the page sends no new
|
Pages that refresh on a timer share one polling rule: the page sends no new
|
||||||
|
|
@ -95,10 +94,11 @@ stream with an error status, the browser gives up and the badge reads
|
||||||
**disconnected**. The **reconnect** button beside it opens the stream again, and
|
**disconnected**. The **reconnect** button beside it opens the stream again, and
|
||||||
the stream replays the rows. The messages page works the same way.
|
the stream replays the rows. The messages page works the same way.
|
||||||
|
|
||||||
The settings menu in the header has a **hide debug output** toggle. It drops
|
The settings menu in the header has a **hide developer details** toggle. It drops
|
||||||
the muted debug rows (thinking, coalesced ticks, harness chatter) from every
|
the muted debug rows (thinking, coalesced ticks, harness chatter) from every
|
||||||
terminal, the preview and the tabs, and rows already on screen hide or show as
|
terminal, the preview and the tabs, and rows already on screen hide or show as
|
||||||
you flip it. The browser keeps the choice for the swarm UI, apart from the
|
you flip it. It also hides the **components** tab, a page for developers, which
|
||||||
|
still answers at `/components`. The browser keeps the choice for the swarm UI, apart from the
|
||||||
per-agent pages'.
|
per-agent pages'.
|
||||||
|
|
||||||
A terminal's own tab takes the agent's icon as its browser-tab icon, so tabs of
|
A terminal's own tab takes the agent's icon as its browser-tab icon, so tabs of
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ that motivated the change.
|
||||||
| `warn` | amber, left rule | stderr, an unrecognised event shape, API retries |
|
| `warn` | amber, left rule | stderr, an unrecognised event shape, API retries |
|
||||||
| `error` | red, left rule | turn failed, a tool result with `is_error: true` |
|
| `error` | red, left rule | turn failed, a tool result with `is_error: true` |
|
||||||
|
|
||||||
The settings menu's "hide debug output" toggle (`getHideDebugPref()`,
|
The settings menu's "hide developer details" toggle (`getHideDebugPref()`,
|
||||||
`@hive/shared/prefs.js`) skips `debug`-level rows entirely rather than
|
`@hive/shared/prefs.js`) skips `debug`-level rows entirely rather than
|
||||||
muting them further — client-side only, no reload. The per-agent page and
|
muting them further — client-side only, no reload. The per-agent page and
|
||||||
the swarm UI both have the toggle, as one shared row
|
the swarm UI both have the toggle, as one shared row
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Settings-menu row: "hide debug output". Rendered as a `<SettingsMenu>`
|
// Settings-menu row: "hide developer details". Rendered as a `<SettingsMenu>`
|
||||||
// child (`./SettingsMenu.js`) by each page that has a terminal, the
|
// child (`./SettingsMenu.js`) by each page that has a terminal, the
|
||||||
// per-agent page and the swarm UI, rather than as a prop on the menu: the
|
// per-agent page and the swarm UI, rather than as a prop on the menu: the
|
||||||
// caller owns its row. Both read the same preference (`../prefs.js`), which
|
// caller owns its row. Both read the same preference (`../prefs.js`), which
|
||||||
|
|
@ -11,7 +11,7 @@ export function HideDebugSetting() {
|
||||||
const [hideDebug, setHideDebugState] = useState(() => getHideDebugPref());
|
const [hideDebug, setHideDebugState] = useState(() => getHideDebugPref());
|
||||||
return (
|
return (
|
||||||
<label class="settings-menu-row">
|
<label class="settings-menu-row">
|
||||||
<span>hide debug output</span>
|
<span>hide developer details</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={hideDebug}
|
checked={hideDebug}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ function DiffBody({ text }: { text: string }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A `debug` row, which follows the "hide debug output" preference as it
|
// A `debug` row, which follows the "hide developer details" preference as it
|
||||||
// changes. Only debug rows subscribe, so a long terminal does not hold a
|
// changes. Only debug rows subscribe, so a long terminal does not hold a
|
||||||
// listener per row.
|
// listener per row.
|
||||||
function DebugRow({ row }: { row: TermRow }) {
|
function DebugRow({ row }: { row: TermRow }) {
|
||||||
|
|
@ -67,7 +67,7 @@ function DebugRow({ row }: { row: TermRow }) {
|
||||||
() => subscribeHideDebugPref(() => setHidden(getHideDebugPref())),
|
() => subscribeHideDebugPref(() => setHidden(getHideDebugPref())),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
// "hide debug output" is a skip, not a dimmer — a hidden debug row
|
// "hide developer details" is a skip, not a dimmer — a hidden debug row
|
||||||
// still occupies no DOM node at all, same as it never streamed, rather
|
// still occupies no DOM node at all, same as it never streamed, rather
|
||||||
// than a CSS `display: none` that would keep it in the layout/DOM for
|
// than a CSS `display: none` that would keep it in the layout/DOM for
|
||||||
// no benefit.
|
// no benefit.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import { FlowEditorPage } from "./pages/notifications/FlowEditorPage.js";
|
||||||
import { NotificationsPage } from "./pages/notifications/NotificationsPage.js";
|
import { NotificationsPage } from "./pages/notifications/NotificationsPage.js";
|
||||||
import { ComponentsPage } from "./pages/ComponentsPage.js";
|
import { ComponentsPage } from "./pages/ComponentsPage.js";
|
||||||
import { JobsPage } from "./pages/JobsPage.js";
|
import { JobsPage } from "./pages/JobsPage.js";
|
||||||
import { HivesPage } from "./pages/HivesPage.js";
|
|
||||||
import { IssueReportPage } from "./pages/IssueReportPage.js";
|
import { IssueReportPage } from "./pages/IssueReportPage.js";
|
||||||
import { ReviewPage } from "./pages/review/ReviewPage.js";
|
import { ReviewPage } from "./pages/review/ReviewPage.js";
|
||||||
import { SchedulesPage } from "./pages/schedules/SchedulesPage.js";
|
import { SchedulesPage } from "./pages/schedules/SchedulesPage.js";
|
||||||
|
|
@ -34,7 +33,6 @@ export function App() {
|
||||||
<Shell>
|
<Shell>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={StartPage} />
|
<Route path="/" component={StartPage} />
|
||||||
<Route path="/hives" component={HivesPage} />
|
|
||||||
<Route path="/alerts" component={AlertsPage} />
|
<Route path="/alerts" component={AlertsPage} />
|
||||||
<Route path="/agents" component={AgentsPage} />
|
<Route path="/agents" component={AgentsPage} />
|
||||||
<Route path="/agents/:name/terminal" component={AgentTerminalPage} />
|
<Route path="/agents/:name/terminal" component={AgentTerminalPage} />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// <StartPage> — `/`, the swarm's landing page: the swarm's name over the
|
// <StartPage> — `/`, the swarm's landing page: the swarm's name over the
|
||||||
// digital-rain backdrop, plus the setup assistant while the swarm has no
|
// digital-rain backdrop, plus the setup assistant while the swarm has no
|
||||||
// `ruth`. The header's swarm name links here. The hive roster that used to be
|
// `ruth`. The header's swarm name links here. The hive roster is a card on the agents page.
|
||||||
// `/` is now `/hives`.
|
|
||||||
import { MatrixRain } from "../shell/MatrixRain.js";
|
import { MatrixRain } from "../shell/MatrixRain.js";
|
||||||
import { useSwarmName } from "../shell/useSwarmName.js";
|
import { useSwarmName } from "../shell/useSwarmName.js";
|
||||||
import { SetupAssistant } from "./SetupAssistant.js";
|
import { SetupAssistant } from "./SetupAssistant.js";
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,14 @@
|
||||||
.ui-agents-view-toggle button:not(.active):hover {
|
.ui-agents-view-toggle button:not(.active):hover {
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The folded hives card over the agents and details panels. */
|
||||||
|
.ui-agents-hives {
|
||||||
|
margin-block-end: 1em;
|
||||||
|
}
|
||||||
|
/* The roster's refresh picker, at the top right of the open card. */
|
||||||
|
.ui-hives-refresh {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-block-end: 0.5em;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import { Button } from "../../ui/button/Button.js";
|
||||||
import { Dialog } from "../../ui/dialog/Dialog.js";
|
import { Dialog } from "../../ui/dialog/Dialog.js";
|
||||||
import { FilterableView } from "../../ui/filterable-view/FilterableView.js";
|
import { FilterableView } from "../../ui/filterable-view/FilterableView.js";
|
||||||
import { Panel } from "../../ui/panel/Panel.js";
|
import { Panel } from "../../ui/panel/Panel.js";
|
||||||
|
import { HivesCard } from "./HivesCard.js";
|
||||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||||
import {
|
import {
|
||||||
RefreshIntervalPicker,
|
RefreshIntervalPicker,
|
||||||
|
|
@ -255,6 +256,9 @@ export function AgentsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div class="ui-agents-hives">
|
||||||
|
<HivesCard />
|
||||||
|
</div>
|
||||||
<SplitView
|
<SplitView
|
||||||
primary={
|
primary={
|
||||||
<Panel
|
<Panel
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// <HivesPage> — the hive roster, `/hives`' content. Fetches
|
// <HivesCard> — the hive roster, a card on the agents page that starts folded.
|
||||||
|
// Fetches
|
||||||
// swarm-controller's `GET /api/hives/status`, the swarm-wide status
|
// swarm-controller's `GET /api/hives/status`, the swarm-wide status
|
||||||
// aggregate: one row per roster hive, freshness derived at read time
|
// aggregate: one row per roster hive, freshness derived at read time
|
||||||
// from the status bucket. Supersedes the earlier `GET /api/hives` +
|
// from the status bucket. Supersedes the earlier `GET /api/hives` +
|
||||||
|
|
@ -15,15 +16,15 @@
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||||
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
||||||
import { Panel } from "../ui/panel/Panel.js";
|
import { Disclosure } from "../../ui/disclosure/Disclosure.js";
|
||||||
import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
|
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||||
import {
|
import {
|
||||||
RefreshIntervalPicker,
|
RefreshIntervalPicker,
|
||||||
type RefreshIntervalMs,
|
type RefreshIntervalMs,
|
||||||
} from "../ui/refresh-interval/RefreshInterval.js";
|
} from "../../ui/refresh-interval/RefreshInterval.js";
|
||||||
import { fetchJson, usePolled } from "../ui/polled/usePolled.js";
|
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
|
||||||
import { Table, type TableColumn } from "../ui/table/Table.js";
|
import { Table, type TableColumn } from "../../ui/table/Table.js";
|
||||||
import { domainCell } from "./hive-domain.js";
|
import { domainCell } from "../hive-domain.js";
|
||||||
|
|
||||||
type Freshness = "fresh" | "stale" | "never_reported" | "unknown";
|
type Freshness = "fresh" | "stale" | "never_reported" | "unknown";
|
||||||
|
|
||||||
|
|
@ -103,14 +104,16 @@ const COLUMNS: TableColumn<HiveStatus>[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 30s default: this page has no inputs to interrupt, and the whole
|
// 30s default: this card has no inputs to interrupt, and the whole
|
||||||
// point of a refresh-interval control is "no manual reload needed" — an
|
// point of a refresh-interval control is "no manual reload needed" — an
|
||||||
// operator who wants it off still can, but the out-of-the-box behaviour
|
// operator who wants it off still can, but the out-of-the-box behaviour
|
||||||
// should actually solve the staleness problem rather than require an
|
// should actually solve the staleness problem rather than require an
|
||||||
// opt-in every visit.
|
// opt-in every visit.
|
||||||
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
||||||
|
|
||||||
export function HivesPage() {
|
// The roster is read on the first open and followed after, so an agents page
|
||||||
|
// where the card is never opened does not poll it.
|
||||||
|
function HiveRoster() {
|
||||||
const [intervalMs, setIntervalMs] =
|
const [intervalMs, setIntervalMs] =
|
||||||
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||||
const { data: hives, error } = usePolled(intervalMs, (signal) =>
|
const { data: hives, error } = usePolled(intervalMs, (signal) =>
|
||||||
|
|
@ -118,17 +121,14 @@ export function HivesPage() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<>
|
||||||
title="hives"
|
<div class="ui-hives-refresh">
|
||||||
icon="🐝"
|
|
||||||
actions={
|
|
||||||
<RefreshIntervalPicker
|
<RefreshIntervalPicker
|
||||||
id="hives-refresh"
|
id="hives-refresh"
|
||||||
value={intervalMs}
|
value={intervalMs}
|
||||||
onChange={setIntervalMs}
|
onChange={setIntervalMs}
|
||||||
/>
|
/>
|
||||||
}
|
</div>
|
||||||
>
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<ApiErrorPanel
|
<ApiErrorPanel
|
||||||
context="failed to load the hive roster"
|
context="failed to load the hive roster"
|
||||||
|
|
@ -145,6 +145,25 @@ export function HivesPage() {
|
||||||
storageKey="swarm-ui:hives:table-filters"
|
storageKey="swarm-ui:hives:table-filters"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Panel>
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HivesCard() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [built, setBuilt] = useState(false);
|
||||||
|
return (
|
||||||
|
<Disclosure
|
||||||
|
title="hives"
|
||||||
|
icon="🐝"
|
||||||
|
hint="the swarm's hives and when each last reported"
|
||||||
|
open={open}
|
||||||
|
onToggle={(next) => {
|
||||||
|
setOpen(next);
|
||||||
|
if (next) setBuilt(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{built ? <HiveRoster /> : null}
|
||||||
|
</Disclosure>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ import type { ComponentChildren } from "preact";
|
||||||
import { Link, useLocation } from "wouter-preact";
|
import { Link, useLocation } from "wouter-preact";
|
||||||
import { LinksMenu } from "./LinksMenu.js";
|
import { LinksMenu } from "./LinksMenu.js";
|
||||||
import { UserMenu } from "./UserMenu.js";
|
import { UserMenu } from "./UserMenu.js";
|
||||||
|
import { useHideDebug } from "./useHideDebug.js";
|
||||||
import { SettingsMenu } from "@hive/shared/settings-menu.js";
|
import { SettingsMenu } from "@hive/shared/settings-menu.js";
|
||||||
import { HideDebugSetting } from "@hive/shared/hide-debug-setting.js";
|
import { HideDebugSetting } from "@hive/shared/hide-debug-setting.js";
|
||||||
import { useApplyThemeOverride } from "@hive/shared/theme-apply.js";
|
import { useApplyThemeOverride } from "@hive/shared/theme-apply.js";
|
||||||
|
|
@ -49,7 +50,6 @@ const THEME_KEY = "swarm-ui:theme-override";
|
||||||
const MOTION_KEY = "swarm-ui:motion-override";
|
const MOTION_KEY = "swarm-ui:motion-override";
|
||||||
|
|
||||||
const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
|
const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
|
||||||
{ href: "/hives", label: "hives", accent: "var(--purple)" },
|
|
||||||
{ href: "/agents", label: "agents", accent: "var(--green)" },
|
{ href: "/agents", label: "agents", accent: "var(--green)" },
|
||||||
{ href: "/alerts", label: "alerts", accent: "var(--red)" },
|
{ href: "/alerts", label: "alerts", accent: "var(--red)" },
|
||||||
{ href: "/jobs", label: "jobs", accent: "var(--pink)" },
|
{ href: "/jobs", label: "jobs", accent: "var(--pink)" },
|
||||||
|
|
@ -209,6 +209,13 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
||||||
// rather than reusing `NavLink`.
|
// rather than reusing `NavLink`.
|
||||||
const { tabs, close: closeTab } = useDynamicTabs();
|
const { tabs, close: closeTab } = useDynamicTabs();
|
||||||
|
|
||||||
|
// The components tab is a developer page: it goes while "hide developer
|
||||||
|
// details" is on. The page itself still answers at `/components`.
|
||||||
|
const hideDeveloper = useHideDebug();
|
||||||
|
const navItems = hideDeveloper
|
||||||
|
? NAV_ITEMS.filter((item) => item.href !== "/components")
|
||||||
|
: NAV_ITEMS;
|
||||||
|
|
||||||
// Applied once here, not inside `SettingsMenu` — every route mounts
|
// Applied once here, not inside `SettingsMenu` — every route mounts
|
||||||
// through this one `<Shell>`, so the override takes effect regardless
|
// through this one `<Shell>`, so the override takes effect regardless
|
||||||
// of which page is showing or whether the menu's ever been opened,
|
// of which page is showing or whether the menu's ever been opened,
|
||||||
|
|
@ -226,7 +233,7 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
||||||
// scroll-offset bookkeeping needed.
|
// scroll-offset bookkeeping needed.
|
||||||
function measureIndex(index: number): IndicatorRect | null {
|
function measureIndex(index: number): IndicatorRect | null {
|
||||||
const navEl = navRef.current;
|
const navEl = navRef.current;
|
||||||
const item = NAV_ITEMS[index];
|
const item = navItems[index];
|
||||||
const textEl = item ? textRefs.current[item.href] : null;
|
const textEl = item ? textRefs.current[item.href] : null;
|
||||||
if (!navEl || !item || !textEl) return null;
|
if (!navEl || !item || !textEl) return null;
|
||||||
const navRect = navEl.getBoundingClientRect();
|
const navRect = navEl.getBoundingClientRect();
|
||||||
|
|
@ -245,8 +252,14 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
||||||
// [c's colour]"). Re-runs on navigation; a separate resize listener
|
// [c's colour]"). Re-runs on navigation; a separate resize listener
|
||||||
// below re-measures the already-settled position without re-hopping
|
// below re-measures the already-settled position without re-hopping
|
||||||
// (a reflow isn't a navigation).
|
// (a reflow isn't a navigation).
|
||||||
|
// The list changed (the components tab came or went), so the settled index
|
||||||
|
// no longer names the same item: land directly on the active one again.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const targetIndex = NAV_ITEMS.findIndex((item) => item.href === location);
|
settledIndexRef.current = -1;
|
||||||
|
}, [hideDeveloper]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const targetIndex = navItems.findIndex((item) => item.href === location);
|
||||||
if (targetIndex === -1) {
|
if (targetIndex === -1) {
|
||||||
settledIndexRef.current = -1;
|
settledIndexRef.current = -1;
|
||||||
setIndicator(null);
|
setIndicator(null);
|
||||||
|
|
@ -278,7 +291,7 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
if (timeoutId !== undefined) window.clearTimeout(timeoutId);
|
if (timeoutId !== undefined) window.clearTimeout(timeoutId);
|
||||||
};
|
};
|
||||||
}, [location]);
|
}, [location, hideDeveloper]);
|
||||||
|
|
||||||
// Fires the render *after* the indicator's first non-null commit —
|
// Fires the render *after* the indicator's first non-null commit —
|
||||||
// deliberately one tick behind, not folded into the effect above,
|
// deliberately one tick behind, not folded into the effect above,
|
||||||
|
|
@ -323,7 +336,7 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
||||||
// label its page registered as a dynamic tab. An unmatched path gets
|
// label its page registered as a dynamic tab. An unmatched path gets
|
||||||
// the bare brand.
|
// the bare brand.
|
||||||
const pageLabel =
|
const pageLabel =
|
||||||
NAV_ITEMS.find((item) => item.href === location)?.label ??
|
navItems.find((item) => item.href === location)?.label ??
|
||||||
tabs.find((tab) => tab.href === location)?.label;
|
tabs.find((tab) => tab.href === location)?.label;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = pageLabel ? `${pageLabel} · ${brand}` : brand;
|
document.title = pageLabel ? `${pageLabel} · ${brand}` : brand;
|
||||||
|
|
@ -355,7 +368,7 @@ export function Shell({ children }: { children: ComponentChildren }) {
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<nav class="shell-nav" ref={navRef}>
|
<nav class="shell-nav" ref={navRef}>
|
||||||
{NAV_ITEMS.map((item) => (
|
{navItems.map((item) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
|
|
||||||
17
frontend/packages/swarm-ui/src/shell/useHideDebug.ts
Normal file
17
frontend/packages/swarm-ui/src/shell/useHideDebug.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// The "hide developer details" preference as a value that follows its toggle:
|
||||||
|
// this tab's, through `setHideDebugPref`, and another tab's, through the
|
||||||
|
// `storage` event. The header uses it to hide the components tab.
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import {
|
||||||
|
getHideDebugPref,
|
||||||
|
subscribeHideDebugPref,
|
||||||
|
} from "@hive/shared/prefs.js";
|
||||||
|
|
||||||
|
export function useHideDebug(): boolean {
|
||||||
|
const [hide, setHide] = useState(getHideDebugPref);
|
||||||
|
useEffect(
|
||||||
|
() => subscribeHideDebugPref(() => setHide(getHideDebugPref())),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return hide;
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ test("the start page is full-bleed", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the hive directory moved off / and is not full-bleed", () => {
|
test("the hive directory moved off / and is not full-bleed", () => {
|
||||||
assert.equal(isFullBleedRoute("/hives"), false);
|
assert.equal(isFullBleedRoute("/agents"), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("an agent's terminal and a subagent's are full-bleed", () => {
|
test("an agent's terminal and a subagent's are full-bleed", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue