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

@ -165,6 +165,75 @@
.shell-nav-indicator.shell-nav-indicator-instant {
transition: none;
}
/* Dynamic tab row — sits below `.shell-header`, its own sticky
strip rather than folded into the header itself: the header's own
sticky offset (`top: 0`) is already claimed, and a header that grows
taller every time a tab opens would shift `.shell-nav`'s measured
positions (`measureIndex`, Shell.tsx) out from under the sliding
indicator mid-animation. `top` below is `.shell-header`'s own
footprint — `0.75em` padding top+bottom either side of the
`2.75em`-min-height nav row (`.shell-nav-link`, this file), so ~4.25em
rounded up to 4.5em for the 1px border + a little slack — not
measured, same fixed-estimate tradeoff `AgentTerminalPage.css`'s own
comment makes for the same reason (not worth a ResizeObserver for one
sticky offset), and the same 4.5em value that file's own height calc
already uses for this header, so the two stay consistent with each
other rather than drifting to two different guesses at one thing.
Undershooting this would mean the sticky header (z-index 10, above
this row's 9) visibly covers the top of the tab row once scrolled —
worth erring high, not low, if the real value ever drifts. */
.shell-tabs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.4em 1.25em;
border-bottom: 1px solid var(--border);
background: var(--bg-elev);
position: sticky;
top: 4.5em;
z-index: 9; /* one below .shell-header's 10, so the header still wins if both ever overlap during a fast scroll */
}
.shell-tab {
display: flex;
align-items: center;
gap: 0.25em;
padding: 0.2em 0.4em 0.2em 0.7em;
border-radius: 0.3em;
background: var(--bg);
border: 1px solid var(--border);
}
.shell-tab-active {
border-color: var(--pink);
}
.shell-tab-link {
color: var(--muted);
text-decoration: none;
font-size: 0.9em;
}
.shell-tab-active .shell-tab-link {
color: var(--fg);
}
.shell-tab-link:hover {
color: var(--fg);
}
/* Same reset every icon-only chrome button in this codebase uses
(`Badge`'s own `<button>` case) — a bare `<button>` here isn't routed
through `Badge` since it's not a status/value pill, just a close
glyph, so the reset is inlined rather than reaching for a component
that doesn't fit this shape. */
.shell-tab-close {
all: unset;
cursor: pointer;
line-height: 1;
padding: 0.1em 0.3em;
border-radius: 0.2em;
color: var(--muted);
}
.shell-tab-close:hover {
color: var(--fg);
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.shell-body {
max-width: 60em;
margin: 0 auto;