New per-row action on AgentsPage: a quiet-variant icon badge (LinkIcon,
matching the LinksMenu/SettingsMenu chrome-not-chip convention) opens a
dialog with an account/token/homeserver form, PUTting
/api/hives/{hive}/agents/{agent}/matrix-accounts/{account} per the
contract atlas posted on hyperhive#3726 (issuecomment-72355).
Built against the contract before the backend endpoint exists per
atlas's explicit note that it doesn't change when the implementation
lands -- this 404s until that item merges. No linked-accounts list:
no route exposes one, and a credential store shouldn't hand a secret
back out anyway, so this is a blind set/update action, matching
mara's 1:1-for-now ruling on the issue.
Verified: tsc --noEmit and nix fmt clean, esbuild build clean. Real
DOM-interaction screenshots against a throwaway mock server (deleted
before this commit, never tracked) -- table column render + disabled
state on a hiveless row, dialog open, form filled with the token
masked, and the success path end to end (token field clears, success
message shows) against a mocked 200 response.
argus caught this reviewing PR #4110: Badge only applied the disabled
prop inside its onClick-present branch (a real <button disabled>).
When onClick is undefined -- exactly the case every current caller
hits when its gating condition is false, since onClick and disabled
are computed off the same condition -- it fell through to a plain
<span> that never reads disabled and never gets the
.ui-badge-interactive class the dimming CSS depends on. WantedMenu
and the matrix-account trigger both silently lost the disabled
affordance to this.
Fixed at the root: both the button-vs-span branch and the
interactive-styling class now key off disabled || onClick (extracted
to one interactive flag) instead of onClick alone.
Verified: tsc --noEmit clean on both swarm-ui and agent (Badge's two
consumer packages), nix fmt clean. Screenshot against a throwaway
mock roster (deleted before this commit, never tracked) showing a
hiveless row and a destroyed row both visibly dimmed now, next to an
enabled row at full opacity -- previously all three looked identical.
argus's review (reproduced, not speculative): .ui-table-scroll sets
overflow-x: auto with overflow-y left unset. Per the CSS overflow
spec, an axis left visible computes to auto too once the other axis
isn't visible -- so this box silently clips vertically as well as
horizontally. The popover was position: absolute; top: 100% under its
header <th>, itself inside .ui-table-scroll -- on a table shorter than
header-plus-popover (a small hive's roster, or any table narrowed by
an existing filter), the popover got cut off at the scroll box's own
bottom edge, with a stray vertical scrollbar as the visible symptom.
Computes the popover's viewport position from the anchor <th>'s own
getBoundingClientRect() and renders it via a portal onto
document.body, position: fixed -- escapes .ui-table-scroll's clip the
same way any position: fixed element escapes an ancestor's overflow
(neither .ui-table-scroll nor .ui-table establishes a new containing
block). Recomputes on scroll (capture-phase window listener, the
standard technique for detecting scroll on a nested scroll container
without binding to every ancestor by hand) and resize while open, so
the popover stays anchored rather than only positioning once at click
time. createPortal comes from preact/compat, already resolvable
through the existing preact dependency -- no new package.json entry.
Verified against argus's own repro shape: a 1-row table, scripted
click on the filter icon (same real-DOM-event technique as this PR's
first round), screenshot shows the popover rendering fully rather than
clipped, no stray scrollbar.
Replaces the permanent filter-row under Table's headers with a small
filter-icon button in each filterable column's own header cell. The
icon fades in on header hover/focus via a CSS opacity transition, or
stays visible outright once that column actually has a filter set
(mara: "instead of a filter row, add little filter icons on header
hover with fade in out animation ... when a filter is set, the filter
icon does not disappear"). Clicking it opens a small anchored popover
directly under the header holding the exact same filter control
filterMode already provides (text input or select) -- the underlying
filter mechanism from hyperhive#4088 is unchanged, only where the
control lives moved. Close-on-outside-click/Escape mirrors the
contract Dropdown already gives its own popover, adapted to a shared
listener across every column instead of a ref per column since only
one popover is ever open at a time.
New FilterIcon in @hive/shared's icons.tsx (a plain inline SVG funnel,
same Feather/lucide-style shape as the existing GearIcon/LinkIcon) --
found and reused that pattern rather than reaching for an emoji glyph,
matching the documented reason those two exist as SVG in the first
place (mara, on the old emoji icons: inconsistent size/weight across
platforms).
Three columns gain Table's filterValue/filterMode (the mechanism
hyperhive#4088 added): title (text, substring search -- there was no
way to search by title text at all), assignees (text, not select --
a row can carry more than one assignee and Table's select mode
matches one whole string per row exactly, so substring search over
the joined string is the shape that actually fits multi-value data),
and blocked (select, synthesized "blocked"/"not blocked" strings --
distinct from the existing "hide blocked" toggle, which only hides
blocked issues and has no way to show only them).
Deliberately not touched: repo (redundant with the existing repo
SelectField), labels (redundant with the existing label chip
multi-select -- chips are the better UI for a bounded label set
anyway), the three numeric columns (no clean filter shape, already
sortable). The existing hand-rolled sort (SortHeader, useLocalSetting-
persisted) is untouched too -- migrating it onto Table's own sortBy
would drop the localStorage persistence this page specifically wants,
and Table doesn't expose controlled sort state to a caller today. The
two layers compose without conflict: Table's own filter/sort runs
over whatever rows it's handed, which is already this page's own
filtered+sorted array.
Scoped on the issue first, including this exact reasoning, before
writing any code.
Dialog previously forced every caller to a fixed width: 90vw; max-width: 44em
shell regardless of content — fine for the create-agent form's wide
two-panel layout (the only caller until now), but a ConfirmDialog's short
paragraph then wrapped at its own narrower max-width while the shell stayed
the wide default, leaving a dead gutter before the close button (mara filed
a screenshot showing exactly this).
First pass added a narrow prop/second CSS class for ConfirmDialog to opt
into a smaller fixed width. Review pointed at the actual root cause one
level up: width: 90vw is a forced width, not a cap — a native dialog's own
UA default is width: fit-content. Switching .ui-dialog to
width: fit-content; max-width: min(90vw, 44em) lets each caller size to its
own content naturally: the create-agent form still hits the 44em cap (same
rendered width as before, confirmed via screenshot), ConfirmDialog's
paragraph settles at its own intrinsic width with no extra prop, no second
CSS class, and no second hardcoded number to keep in sync with the first.
Table columns whose value only ever comes from a small closed set
(freshness, wanted, hive) get a <select> in their filter-row cell
instead of a free-text input, populated from the distinct values
present in the currently-loaded rows plus an "any" option, matched by
exact equality instead of substring. Free-text columns (name, the
agent's own status message, config PR, hive domain) are unchanged.
mara, hyperhive#4079: "agent wanted state is multiple buttons insteaf
of a badge with dropdown ... same pattern as agent term badges with
dropdowns". The wanted column used to be a toggle badge plus a
separate quiet destroy badge, stacking under each other in the narrow
column. Replaced both with one WantedMenu badge that opens a Dropdown
with the three explicit states (up/offline/destroy) -- the exact
badge-triggers-a-dropdown shape the per-agent terminals StatusChips
already uses (and swarm-uis own ComponentsPage already demos with
sample data), built from the same shared Badge/Dropdown components.
"up" still declares straight away with no confirmation; "offline" and
"destroy" still go through the existing ConfirmDialog modals unchanged
-- only the trigger moved, the confirm behavior for the two directions
that already had one is untouched.
Explicit dropdown options also fix a real bug the old toggle had:
mara also asked "when no state is declared, i want to set it to
online" -- the old toggle inferred a target as the opposite of
snapshot.running for an undeclared row, so a click on an
undeclared-but-running agent silently declared it offline rather than
making its actual state explicit. The dropdown just lets "up" be
picked directly regardless of any inference, which is what she is
asking for -- flagging this reading explicitly in case an actual
one-time migration (auto-declaring every currently-undeclared agent
up) was intended instead, which this does not do.
Added a shared .ui-dropdown-anchor utility class to Dropdown.css --
this is the third near-identical "position: relative wrapper for a
badge that opens a Dropdown" (after agents own StatusChips.css and
swarm-uis ComponentsPage.css), so a new caller should not reinvent a
fourth copy. Left the two existing ones alone rather than migrating
them as a drive-by.
Verified with a local esbuild build + a throwaway mock /api/agents/status
server, screenshotted headlessly: the wanted column now shows exactly
one badge per row instead of stacked badges.
argus, review: the primary wanted-toggle badge still had no
wanted === "destroyed" guard, so it stayed clickable on a destroyed
row and would PUT {state: "up"} on click -- directly contradicting
the destroy confirm dialogs own "not reversible from here" copy.
Missed this in the previous fix-up (which only addressed the
ConfirmDialog-for-stop ask). Added the guard to both the click handler
and disabled, plus a title explaining why on a destroyed row.
mara: "use the new component where we already have confirm dialogs."
The stop/offline direction of the wanted toggle used a native
window.confirm -- the reasoning at the time was that a hand-rolled
Dialog felt heavy for a reversible action, with the real Dialog confirm
reserved for the irreversible destroy direction. Now that
ConfirmDialog is a one-line-per-caller shared component, that argument
no longer buys consistency anything, so both directions confirm the
same way.
mara, reviewing the swarm-ui destroy-trigger PR: "why the extra
styling? shouldnt there be a component that does this already?" There
wasnt one -- Dialog is deliberately content-agnostic (see its own
file-top comment), so the destroy confirm had grown its own
page-scoped .agents-destroy-confirm/-actions CSS for what is really a
generic "message + cancel/confirm button row" shape.
Extracted ui/confirm-dialog/ConfirmDialog.tsx: wraps Dialog, owns the
button row, leaves the message body to the caller via children.
AgentsPage now uses it instead of a bare Dialog + bespoke CSS; deleted
the now-unused AgentsPage.css.
Closes#4067. Backend half (Wanted::Destroyed + reconcile) shipped in
#4065 with no swarm-controller API changes needed -- SetAgentStateRequest
already accepted {"state": "destroyed"}, it just had nothing in swarm-ui
sending it.
Destroy is a separate `quiet`-variant badge next to the existing
start/stop toggle (#3988's "wanted" column), not a third state folded
into that same click target -- one wrong click on a shared toggle
would be irreversible, where a dedicated badge only fires from its own
confirm dialog. That confirm is a real `Dialog`, not the native
`window.confirm` the reversible stop direction uses -- the wanted
column's own comment called this out as the case that would justify
one when it was first written.
Shared the PUT-declare/pending/error/patch-rows logic between the
existing toggleWanted and the new destroyAgent (declareState) rather
than duplicating it -- confirmation and target-state selection are the
only parts that differ between a toggle and a one-way declaration.
Verified: `tsc --noEmit` clean, `nix fmt` reports the expected
formatting-only diff, wire shapes (state string "destroyed",
AgentDeclaration response) checked against swarm-queue-client's
AgentState::as_str and swarm-controller's actual handler rather than
assumed from the issue description.
docs/web-ui.md duplicated the web-ui/ directory name at the top level --
the only such collision in docs/ (every other subsystem has just a
directory, no sibling <dir>.md file). That's exactly why it rendered
outside the directory structure in the docs site nav (mara's report,
hyperhive#4054): the site build walks docs/ generically with no
special-casing, so a loose top-level file next to a same-named
directory shows up as its own flat top-level entry instead of nesting
under that directory's section.
web-ui.md's own first paragraph already said as much -- 'This doc has
been split for readability... start at web-ui/README.md instead.' It
was a leftover pointer from before the split, not a page carrying
unique content on its own merit.
Folded its two sections web-ui/README.md didn't already have (the
swarm-ui design-guide link, and the task-oriented 'reading paths'
quick-lookup list) into web-ui/README.md's existing 'More depth'
section, then deleted the stray file and repointed every real
reference at it: 3 in-tree doc cross-links, 3 doc prose mentions
(retargeted to the more specific dashboard.md/shape.md sub-page each
one was actually about), and ~28 frontend source comments
(dashboard/agent/shared packages) that cited it as
'docs/web-ui.md::<heading>' for implementation context -- retargeted
each to whichever of dashboard.md/shape.md/agent.md actually carries
that heading now, verified against each file's real heading list
rather than guessed.
Verified via scripts/check-doc-refs.sh (the same lint CI runs): 0 dead
pointers, both before write (confirming the tree was clean beforehand)
and after (confirming nothing broke).
hyperhive#4020, mara. TableColumn gets two new optional extractors,
each one the whole signal for its capability (mara: 'why separate
selector and flag?' — dropped the sortable/filterable booleans that
would've said the same thing twice and could disagree with the
extractor's presence):
- sortBy: (row) => string | number — column is click-to-sort iff
present. Table owns the sort state (one active column, header click
cycles none -> ascending -> descending), since a rendered cell often
isn't the sortable value itself (e.g. AgentsPage's status column
renders a Badge, not a plain string).
- filterValue: (row) => string — column is filterable iff present. A
text input row under the headers, substring match case-insensitive.
Client-side only, no backend change - every consuming page already
fetches its full row set. Wired into AgentsPage and HivesPage, the two
pages with a real per-row Table. Left IssueReportPage alone (already
has its own purpose-built sort + label/hide-blocked filters, predates
this and covers its own domain better than a generic per-column text
filter would) and JobsPage alone (renders an indented state tree via
JobqGraph, no column table at all despite what my original scoping
comment assumed).
Verified: typecheck + build clean, nix fmt clean, static render of the
actual built CSS confirms the new sort-header + filter-row markup
doesn't break table layout.
mara: 'separate technical status from agent provided status string' —
the agents page's status column concatenated the freshness label with
the agent's own free-text status_text into one Badge pill, meant for a
short discrete label, not a full sentence. Long status strings blew
the row out and made the table look messy.
Now two columns: 'status' is just the freshness badge (fresh/stale/
never reported/not in swarm identity) plus relative time, unchanged in
meaning; 'message' is the agent's own status string as plain prose in
its own capped-width, wrapping cell (Table's new cellClass, ui-table-
prose), not a badge. A stopped agent (status_text always null per the
wire contract) shows an em dash there instead of a stale message.
Verified against a static render of the built CSS (real dark theme,
real Badge/table classes, the exact long strings from the screenshot
mara attached) — wraps cleanly within the column instead of
overflowing. Typecheck + build clean.
hyperhive#4008, mara: 'add a setting to not display verbose output...
like the grey colored debug stuff.'
Same shape as the existing expand-tool-output preference
(ExpandDetailsSetting.tsx): a new shared/src/prefs.ts key pair
(getHideDebugPref/setHideDebugPref), a new settings-menu-row component
owning its own state (HideDebugSetting.tsx, not a prop threaded through
the shared SettingsMenu component — per mara's earlier review on the
first one, more per-page options as props there is how that component
accumulates cruft), mounted next to ExpandDetailsSetting in Root.tsx.
Row.tsx skips (returns null for, not CSS display:none) any TermMsg
whose level is 'debug' when the pref is set — matches the muted 'debug'
row this issue is about (see docs/web-ui/terminal-rendering.md's Levels
table). Read live per-row, same as expand-details, so toggling applies
to newly streamed rows in an already-open tab without a reload; already
-rendered rows are unaffected either way, same non-retroactive
precedent the existing preference already sets.
An agent whose freshness is "unknown" is a distinct case, not a
fallback: it reported into the KV bucket but isn't in the swarm-identity
roster. The label used to just reuse the enum name ("unknown"), which
doesn't tell an operator what to do about it. Give it a label that
names the actual situation ("not in swarm identity") plus a tooltip
spelling out the fix (register/migrate it), so an operator scanning the
roster during a migration can immediately tell which agents still need
work.
No backend change needed — GET /api/agents/status already emits a row
for every hive-reported agent outside the roster (AgentStatusReader,
unit-tested as an_agent_outside_the_roster_is_surfaced_as_unknown), and
the page already renders every row it gets back.
hyperhive#3896. The backend for start/stop (Up/Offline wanted-state
declarations) already existed and was merged (#3905's writer, the
PUT /api/hives/{hive}/agents/{agent}/state route) — nothing here was
waiting on Paused/Destroyed, which I'd mistakenly conflated with this
issue in an earlier comment (that's #3803, a different feature).
swarm-controller: merges each row's declared wanted state into
GET /api/agents/status, same shape as the config_pr merge (one read
per distinct hive, not per agent, since a declaration is a hive's
whole agent map).
swarm-ui: AgentsPage gets a "wanted" column — clicking the current-
state badge toggles it (Badge's own chip-plus-control shape, same as
its own header comment's pause/resume example), backed by the PUT
route above. A row with no declaration yet reads its implied current
state off the agent's own last-reported running flag. Stop asks for
confirmation (native window.confirm — no confirm-dialog component
exists in swarm-ui yet); start doesn't.
Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.
Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.
None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
Per mara's review call on this PR: "the view should be filled by a single
backend call." AgentsPage.tsx was doing three fetches (/api/agents,
/api/config-prs, /api/agents/status) and joining them client-side by name.
Moves the config-PR join server-side instead: AgentStatusRow gains a
config_pr field, populated by get_agents_status's handler from
AppState::config_prs after agent_status::AgentStatusReader::view() returns
- not inside that module, which has no forge client and stays that way (see
the field's doc comment for why the handler is the right layer for this
merge, not the reader).
AgentsPage.tsx now does exactly one fetch and no client-side joining at all
- the wire row is the table row. Dropped the separate AgentStatusRow TS
interface (folded into AgentRow, which now mirrors the backend type
field-for-field) and the /api/agents + /api/config-prs fetches entirely;
neither is needed once /api/agents/status already returns every roster
agent with its config PR attached.
ConfigPrStatus gained Deserialize (previously Serialize-only) since
AgentStatusRow derives both and a struct's derive requires every field to
support it.
Continues #3341 item 3, unblocked now that #3568/#3569 (items 1/2)
are merged and GET /api/agents/status is live.
Third fetch alongside the existing roster + config-PR ones, joined
client-side by name same as the config-PR merge. Adds a hive column
and a status column (freshness badge + status_text + relative-time,
same rendering AgentsPage's sibling HivesPage already uses for the
hive-level status endpoint).
`docs/` was reorganised into topic subdirectories and the references to it
were not moved with it. Thirteen distinct paths and three relative links no
longer resolved, spread across nix, css, html, js, markdown and
.prettierignore — a stale pointer is not a markdown problem, it is wherever
someone wrote a path down.
Each mapping resolved to exactly one target. `docs/matrix.md` was the sole
ambiguous basename: .prettierignore lists `docs/tools/matrix.md` separately
and that entry still resolves, so the stale one is the integrations doc.
The three relative links were each one `../` too deep — from `<crate>/src/`
two levels reach the repo root. `hive-agent/src/login.rs` already had the
correct form, in the same crate, at the same depth.
.prettierignore is repointed rather than dropped, though nothing in the tree
runs prettier: no treefmt entry, no CI job, no package script. Whether that
config should exist at all is a separate question from whether it names
files that do.
Continues #3901 (dashboard round 1 was #3906, tabs.js/swarm.js).
Scanned the rest of packages/dashboard/src for the same 'moved to X'/
'now lives in Y' pattern: five more hits in schedules.js, common.js,
logs.js (two, one duplicating the other), core.js, and swarm.js.
All trimmed to state the current location as a fact rather than
narrating the move; one inline duplicate (logs.js's tab-default
comment restating the file-header pointer) dropped outright.
`check-doc-refs.sh` resolves the PATH half of a `docs/x.md::Section`
pointer and stops there. The section half rots the same way, and more
quietly: the file still exists, so every path-shaped check stays green
while the pointer names nothing.
Eighteen sites, five distinct pointers, each retargeted at a heading
verified to exist rather than at the nearest plausible one:
docs/web-ui.md::Container row
-> docs/web-ui/dashboard.md::Container row
docs/web-ui.md::Shared terminal pane
-> docs/web-ui/shape.md::Shared terminal pane
Both sections moved out when docs/web-ui.md became a two-heading
index. The path still resolves, which is exactly why nothing
caught them.
approvals.md::Helper events to the manager
-> approvals.md::Helper events to the submitting agent
Renamed with the manager special-casing removal; the pointer kept
the old vocabulary.
approvals.md::Migration from the pre-tag
-> approvals.md::Startup migrations (older hosts)
Same content, including the HIVE_SKIP_META_MIGRATION kill switch
the citing comment names.
agent-hierarchy.md::Current state
-> ::Where the tree lives (topology.rs, container_view.rs)
-> ::Reparenting (topology.rs's set_parent, host-sock)
Split by what each site actually asks for rather than repointed
uniformly: two want the format and the source-of-truth rule, two
want the reparenting validation.
Three known-dead pointers are deliberately left alone:
* `docs/integrations/forge.md::Sources` sits on a line PR #3927
rewrites; fixing it here would conflict for no gain.
* `docs/web-ui/shape.md::One unified channel` names real text that is
bold inline rather than a heading — which of those counts as
resolvable is the open question on #3922.
* `persistence.md::Harness state files` should point at a heading
whose own text contains backticks, and the backticked-pointer form
cannot nest them. That is a limit of the convention, not a typo.
Comments only; no behaviour change. Refs #3922.
Comments cite nix modules, scripts and crate source files constantly,
and nothing evaluates a comment — so when a file moves, the reference
rots silently and `nix flake check` stays green. A reader following one
finds nothing and cannot tell whether the file was renamed, deleted, or
never existed.
Seven such references, each repointed at the file that actually holds
the thing the sentence is about rather than at the directory the old
name became:
hive-c0re/src/agent_config/limits.rs hive-agent/src/mcp.rs
-> hive-agent-mcp/src/mcp/mod.rs
hive-agent-mcp/src/mcp/mod.rs hive-c0re/src/limits.rs
-> hive-c0re/src/agent_config/limits.rs
(and the module path in the doc
comment above it, which was stale
in the same way)
hive-c0re/src/forge/mod.rs hive-c0re/src/knowledge.rs
-> hive-c0re/src/workers/knowledge.rs
nix/host-modules/hive-c0re/options.nix hive-c0re/src/hive_stats.rs
-> hive-c0re/src/stats/hive_stats.rs
nix/packages/default.nix nix/host-modules/hive-c0re.nix
-> .../hive-c0re/options.nix
nix/agent-modules/network.nix nix/host-modules/hive-gateway.nix
-> .../hive-gateway/dnsmasq.nix
frontend/README.md nix/modules/frontend.nix
-> nix/packages/frontend.nix
The two `limits.rs` comments are a matched pair: each names the other's
old path, so the "keep in sync" instruction they exist to carry pointed
both ways at nothing.
Where a flat module became a directory the target is the file that
declares the named thing, not `default.nix` by reflex — the
`preBuildAgentTemplates` option is declared in `options.nix`, and the
DHCP pool that sentence is about lives in `dnsmasq.nix`.
Comments only; no behaviour change. Refs #3923, which is about whether a
gate should cover this class at all — that question is unanswered and
this does not close it.
Continues #3901 (dashboard/#3906, agent+swarm-ui/#3917, this closes out
the packages/shared slice — jobq-graph was already done separately).
Scanned the rest of shared/src for the same 'used to be X, now Y'
pattern: one real hit. The other two matches (hive-btn.js's is=
attribute history, hive-side-panel.js's one-word /* legacy */ comment)
are load-bearing or too trivial to touch.
Continues #3901. Same pattern as tabs.js/swarm.js: cut 'used to be
X, now Y'/'moved to'/'no longer' change-history narration down to
the current design fact, keep every load-bearing rationale intact
(the ResizeObserver feedback-loop note in Header.tsx, the Dropdown-
vs-real-<a> semantics in MetaNav.tsx, etc.).
Two of the three motion gaps mara flagged on the swarm-ui jobs graph
(the third, node status changes as a tree of nesting divs rather than
a node-link diagram, has no edges to animate today — see the issue
thread for that scoping correction).
- New node mount: .jg-node gets a fade+slide-in keyframe. No JS change
needed — Preact only creates a new .jg-node DOM node when its key
(the node id) is genuinely new, so this only plays on first
appearance, not every fetch re-render.
- State change flash: NodeView tracks each node's previous state via
a ref; on a real change it adds .jg-state-flash to the glyph span
(removed on animationend), driving a scale pulse.
Both follow the same three-rule motion-guard shape as Shell.css's
shell-page-enter / LinksMenu.css's links-menu-popover-enter.
Per mara's guidance on hyperhive#3901 (target ~15% comment density
overall, less where obvious, more where not; prefer docs for
abstract/general topics; don't restate facts in multiple places;
don't document history).
tabs.js: removed 4 pure "X now lives in Y" / "moved to Z" historical
asides (the underlying facts are already documented in
docs/web-ui/dashboard.md, not lost by removing the floating in-code
aside) and merged one comment block that had drifted into restating
the same fact twice (one ticker feeding two live displays, documented
as if it were two separate tickers).
swarm.js: cut a comment narrating the removal history of two features
that no longer exist in the code (a per-agent queued-badge and a
client-side jobq tally), keeping only the design constraint still in
force (why the jobq-derived state here is deliberately narrow); cut a
comment documenting a removed CSS class's history down to a statement
of the current class's purpose; cut a comment restating the
jobq-rollup rendering rationale already stated once, above, down to a
one-line pointer; trimmed a "legacy flat layout is bit-identical"
history clause down to what the depth-0 case actually renders.
mara (#3877): issue report too narrow — the 8-column table was
clipped by .shell-body's 60em readable-line-length cap, which is right
for the rest of the UI's cards/forms but too tight for a wide table.
damocles diagnosed the root cause and scoped the shape (route
allowlist + an additive .shell-body-wide modifier, no prop plumbing
through App.tsx) before asking for a nod; mara then routed the actual
build to me. Implemented that shape as scoped: /issues opts into a
90em cap via WIDE_BODY_ROUTES, every other route keeps 60em untouched.
Verified with real headless-chromium screenshots at 1280px: the issue
report's full 8-column row (through 'transitively blocks') now fits
with no horizontal scroll, and a second screenshot of /agents confirms
every other route is unaffected.
mara asked on PR #3854 to remove the revive action alongside the
blank-name spawn form. Both posted to /api/request-spawn as the last
two frontend callers; with this the dashboard has no UI path left that
hits that endpoint (backend removal/decision stays damocles's call).
Drops the K3PT ST4T3 tombstone row's ⊕ R3V1V3 form, leaving PURG3 as
the only per-tombstone action. Updated the two doc/comment spots that
described the now-gone button.
Frontend half of removing hive-level agent spawning (swarm-level
creation now covers it, and its forge-works confirmation just landed
in hive/hive-chat). Removes call.js's blank-name spawn-request form
(R3QU3ST SP4WN) and its now-orphaned .spawnform CSS.
Deliberately keeps core.js's tombstone-list revive action (R3V1V3) --
a different feature (respawn an *existing* agent, reusing its kept
state), not agent creation, and not what this issue asked to remove.
It happens to post to the same /api/request-spawn action with the
tombstone's own name pre-filled rather than a blank input.
Backend endpoint + wiring is a separate slice (damocles).
claude's real OAuth URL is one long unbroken query-string token, no
whitespace for the browser to break on. Without overflow-wrap the
<a> just kept going past the login card's right edge instead of
wrapping at the card's bounded width (mara reported this with a
screenshot). overflow-wrap: anywhere lets it break mid-token as a
last resort, scoped to just the URL line (.login-url) rather than
the whole card.
argus's review on the original fix: refresh() did a parallel one-off
fetch instead of calling the mount effect's self-rescheduling poll(),
so it never re-armed timerRef after firing. Wiring refresh() to run on
every visibilitychange-to-visible meant the very first tab-switch back
into focus would kill periodic polling for the rest of the session --
reproducing the exact staleness bug this branch set out to fix, just
delayed by one tab switch instead of immediate.
Fix: hoist poll into a ref set by the mount effect so refresh() invokes
the same self-rescheduling function rather than a parallel fetch that
drops the loop. Also gave the visibilitychange effect an empty
dependency array per the review's second note -- it only closes over
stable refs/setters, and now that Root's 1s ticker re-renders the
calling component every second, a deps-less effect would tear down and
reattach the listener that often for no reason.
The status badge's elapsed-time text ('thinking Xm Ys') was computed
from Date.now() inside the render function, so it only ever advanced
when the component actually re-rendered. Two gaps let it go stale:
- useAgentState's poll loop is a chained setTimeout, which browsers
throttle (or suspend outright) once the tab is backgrounded, so
polling could stall for a long time with no way back to a live
reading short of a full page reload.
- even under healthy polling, the age text only advanced once every
~4s (the poll interval) instead of counting up smoothly.
Fix: resync immediately on visibilitychange (so returning to a
backgrounded tab doesn't leave a stale reading), and drive the age
text off its own 1s interval independent of the poll cadence.
Two non-blocking notes from review:
- the /api/repos effect now uses the same cancelled guard the
repo-filter effect already has, so an unmount mid-flight doesn't call
setRepos/setError on a gone component.
- Table's TableColumn gains an optional ariaSort field, consumed as the
<th>'s aria-sort attribute; the issue-report page's sortable columns
now report ascending/descending/none so a screen reader can announce
which column and direction is active, not just the sighted ▲/▼ glyph.
damocles caught it on the swarm-controller PR: forge's assignee field
is legacy single-value, assignees is the real multi-assignee list, and
this repo actually uses multiple. Widen the frontend's row type +
column to match -- rendered comma-joined the same way labels already
are.
New /issues route: a sortable, filterable table over open issues across
every repo that has one -- repo picker (default: no filter, every repo
combined), hide-blocked toggle, and a label multi-select, consuming
swarm-controller's GET /api/repos + GET /api/issue-report / GET
/api/repos/{org}/{repo}/issue-report (see hyperhive#3831 for the row
shape). blocked and depended_on_by_count arrive pre-resolved per row --
this page does no dependency-graph walking of its own, just sort/filter
over what it's given. Default sort is depended_on_by_count descending,
matching mara's framing of the report's headline ordering.
Widened Table's TableColumn.header from string to ComponentChildren so
a column can carry a real clickable sort-toggle button instead of
forking a second table primitive for this one page.
mara (issue #3817): remove the home link + page, put the setting toggle into the Y3R C4LL tab. Deleted settings.html/.js/.css; the browser-notification toggle (the only content there) now renders as a ◆ PR3F3R3NC3S ◆ section under Y3R C4LL's approvals/inbox, wired the same way (NOTIF.bind()/NOTIF.show() in common.js — no behavior change, just a new mount point). Updated build.mjs's entry lists and every doc/comment that pointed at the old page.
mara (issue #3816): remove the swarm-service links (forge, matrix) from the hive home hub. Both surfaces are still reachable directly (forge's own public URL, /matrix/); they just no longer get a tile on H0M3. Dropped the now-dead reveal/href-fill logic in home.js and updated the two docs that described the old gating.
Don't state what a function/module doesn't do and where that
happens instead — just describe what it does. Cut the "not
something this function decides" / "not affected by this" /
"not a placeholder for a later commit" asides from the doc
comments touched in the last two commits.
ClassifyCtx's tool_use-id correlation gates markdown-vs-plain body
format for a recv result, not open/collapsed state — that's always
the operator's uniform preference now. 5 backend comments still
described it as controlling "default-open" rendering, contradicting
the actual render path and this PR's own rewritten docs.
Also fixed useAgentState.ts's stale comment promising an
SSE-triggered refresh model "in a later commit" — that's permanently
off the table now that the terminal stream's kind tag is gone by
design (argus flagged this as a drive-by, not blocking, but it's a
one-line cause-and-effect of this same PR so fixing it here).
Per review: StreamRow was meant to match what the server sends in
TermMsg, not be a separate model needing a translation step.
- classifyEvent.ts and streamRow.ts deleted; termMsg.ts holds the wire
types (TermMsg/TermEnvelope) plus TermRow, a TermMsg with just the
key/fromHistory bookkeeping Preact needs for list rendering.
- Row.tsx renders a TermRow directly: level -> CSS class, empty
summary + markdown body -> flat row, everything else with a body ->
expandable details gated by the operator's preference. No separate
classification step.
- useLiveStream.ts drops ClassifyCtx (a single incrementing key
counter didn't need a whole context object) and maps envelopes to
rows inline.
- docs/terminal-rendering.md trimmed substantially — was documenting
more implementation detail than useful; points at stream_enrich.rs
for the per-tool specifics instead of duplicating them in prose.
Move terminal-row classification server-side into a new
hive-agent/src/term_msg.rs, replacing the old JSON-mutation
enrich()/stamped-field approach in stream_enrich.rs with one
uniform wire shape: {icon?, level: debug|info|warn|error, summary,
body?, body_format?: markdown|diff, coalesce_key?}. No more per-row
`kind` tag or raw claude-JSON passthrough — every row is the same
shape, with structural identity carried by icon + summary text
instead of a CSS class per row kind.
hive-agent/src/web_ui/stream.rs's history + SSE endpoints now both
call term_msg::classify() and serve TermEnvelope{ts, seq?, msgs}
frames; events that classify to zero rows (agent-state changes,
drop-noise) never reach the wire.
Frontend: classifyEvent.ts collapses from a large per-tool dispatch
tree to a thin TermMsg -> StreamRow adapter. streamRow.ts/Row.tsx
drop the now-dead meta/childText fields. terminal.css switches from
a dozen-odd per-row-kind classes to four level-based color rules.
Expand/collapse of a bodied row is now a uniform client-side
decision (the operator's preference), no server-side per-tool
override.
docs/terminal-rendering.md rewritten to match.
argus, reviewing PR#3793: a third stale ❓ ask mention survived in the
same file/table the first pass touched (docs/terminal-rendering.md's
icon legend) — tool_icon() has no ask/answer arm at all, confirmed by
reading the function directly. Swept the whole tree this time instead
of trusting the earlier narrow grep: found three more —
docs/web-ui/dashboard.md's S3TT1NGS section still documented the
expand-tool-output toggle as living on the dashboard, which moved to
the per-agent page's own SettingsMenu popover in #3780 and was never
followed up here; markdown.ts/streamRow.ts/terminal.css had the same
send/ask/answer/recv phrasing as the original two comments.