Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: full-tab agent terminal (#4506)

Adds a full, non-capped agent terminal reachable from a new expand
trigger on the embedded AgentTermPreview (the detail-panel preview on
AgentsPage stays as-is, just gains the trigger). Opens
/agents/:name/terminal in a new dynamic tab in Shell's header, next to
the static nav row — tabs persist across a reload via useDynamicTabs,
a small localStorage-backed hook built on @hive/shared's existing
settings-storage primitive.

AgentTermPreview gains two new props to support both mounts from one
component: fullHeight (drops the 12em preview cap, fills its page)
and showHeaderBadges (default true — lets a future caller that
already shows turn_state/model/ctx/cost elsewhere suppress this
cluster; AgentsPage doesn't use it, see below).

Deviation from the originally posted plan (issue comment 80596): that
plan proposed AgentsPage's embedded preview pass showHeaderBadges as
false, reasoning the detail panel already duplicates that info.
Checked the actual code before implementing — it doesn't; AgentRow/
AgentTypes.ts carry none of turn_state/model/ctx/cost, and
AgentTermPreview's own floating badges are the only place swarm-ui
shows them. Left the badges visible there instead of shipping a
regression the plan's own stated justification didn't hold up to.

Also fixed a same-tab pub/sub race found by actually rendering a cold
load of /agents/:name/terminal (headless chromium, not just reasoning
about the code): useLocalSetting subscribes inside a useEffect, and
mount effects fire children-before-parents, so a descendant's
mount-time write (AgentTerminalPage registering its own tab) can beat
an ancestor's (Shell's) subscription into existence, leaving Shell's
tab row silently empty on a direct/reload load. Fixed by having
useDynamicTabs re-sync from storage on every location change, not
just on notify() — the fix lives in the new hook itself, not in the
shared settings-storage primitive theme/motion overrides also use.
This commit is contained in:
iris 2026-09-28 21:43:29 +02:00 • committed by mara
commit c460e91bd1

View file

@ -0,0 +1,42 @@
// <AgentTerminalPage> — the swarm terminal tab's full page, mounted at
// `/agents/:name/terminal`. The MVP `AgentTermPreview` (below `AgentsPage`'s
// detail panel) is explicitly capped and read-only; this is that same
// component's `fullHeight` mount — no 12em cap, floating header badges
// shown (there's no detail-panel `<dl>` here to duplicate them). Sending
// input back to the agent is still explicit follow-up scope, same as the
// preview's own file comment says — this page doesn't change that.
//
// Registers itself in the dynamic-tabs row on mount (not just when opened
// via the preview's expand trigger) — a bookmark, a shared link, or a
// plain page reload should all re-open the tab instead of landing on a
// page with no way back into the tab row. `useDynamicTabs().open()` is
// idempotent (dedupes by href), so this is safe to call every mount.
import { useEffect } from "preact/hooks";
import type { RouteComponentProps } from "wouter-preact";
import { AgentTermPreview } from "./AgentTermPreview.js";
import { useDynamicTabs } from "../../shell/useDynamicTabs.js";
import "./AgentTerminalPage.css";
export function AgentTerminalPage({
params,
}: RouteComponentProps<{ name: string }>) {
const { open } = useDynamicTabs();
const name = decodeURIComponent(params.name);
// Runs once per mounted agent name — re-registering on every render
// would just be redundant work against an already-deduped list, but
// there's no reason to pay it more than once per identity.
useEffect(() => {
open({ href: `/agents/${encodeURIComponent(name)}/terminal`, label: name });
// eslint-disable-next-line react-hooks/exhaustive-deps -- `open` is a
// fresh function identity every render (`useDynamicTabs` doesn't
// memoize it); including it would re-fire this effect every render
// instead of once per agent `name`.
}, [name]);
return (
<div class="ui-agent-terminal-page">
<AgentTermPreview agentName={name} fullHeight showHeaderBadges />
</div>
);
}