Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: terminal tab fills the viewport below the header

The agent terminal tab now takes the whole window except the header bar,
full width and height, instead of sitting in the padded content column.
The connection badge floats inside the terminal's top-left corner and the
toolbar row above it is gone on this route.
This commit is contained in:
iris 2026-10-04 15:52:24 +02:00 • committed by mara
commit 66df552f68
5 changed files with 87 additions and 51 deletions

View file

@ -8,16 +8,11 @@
margin-block-start: 1rem;
}
/* Holds the connection-status badge + (unless `fullHeight`) the
expand-to-fullscreen trigger — `margin-left: auto` targets the expand
trigger by its OWN class, not `:last-child` (tried that first; it
silently broke when `fullHeight` hides the expand trigger, since the
status badge then becomes the `:last-child` too and gets pushed to
the right edge instead of staying flush left — caught by actually
rendering the `fullHeight` case, not just reasoning about the CSS).
Same split-row intent `Shell.css`'s `.shell-header-actions` already
uses for its own pair of triggers, just addressed directly instead of
positionally. */
/* Holds the connection-status badge and the expand-to-fullscreen
trigger (absent from the `fullHeight` mount, which floats the status
badge inside the terminal instead). `margin-left: auto` targets the
expand trigger by its own class rather than `:last-child`, same
split-row intent `Shell.css`'s `.shell-header-actions` uses. */
.ui-agent-term-preview-toolbar {
display: flex;
align-items: center;
@ -51,6 +46,7 @@
.ui-agent-term-preview-full {
display: flex;
flex-direction: column;
margin-block-start: 0;
}
.ui-agent-term-preview-full .ui-agent-term-preview-wrap.terminal-wrap {
max-height: none;
@ -61,6 +57,14 @@
max-height: none;
height: 100%;
}
/* Flush to the header bar and the viewport sides; the input row below
gets its own padding so it doesn't touch the screen edge. */
.ui-agent-term-preview-full .ui-agent-term-preview-wrap.terminal-wrap {
margin: 0;
}
.ui-agent-term-preview-full .ui-agent-input {
padding: 0.4em 0.75em 0.6em;
}
/* `box-sizing: border-box`, not the browser's default `content-box` —
`.live.terminal`'s own `padding: 0.8em 1em 0.4em` (terminal.css) is
@ -105,3 +109,12 @@
gap: 0.25em;
max-width: calc(100% - 0.8em);
}
/* Full-tab mount's connection badge — top-left floating twin of
`.ui-agent-term-preview-header-badges` (top-right), same anchor. */
.ui-agent-term-preview-live {
position: absolute;
top: 0.4em;
left: 0.4em;
z-index: 1;
}

View file

@ -175,13 +175,16 @@ export function AgentTermPreview({
(fullHeight ? " ui-agent-term-preview-full" : "")
}
>
<div class="ui-agent-term-preview-toolbar">
<Badge
class="ui-agent-term-preview-status"
tone={CONNECTION_TONE[connection]}
value={CONNECTION_LABEL[connection]}
/>
{!fullHeight && (
{/* The full-tab mount has no toolbar row: the terminal takes the
whole page below the shell header, and the connection badge
floats inside it with the other badges (below). */}
{!fullHeight && (
<div class="ui-agent-term-preview-toolbar">
<Badge
class="ui-agent-term-preview-status"
tone={CONNECTION_TONE[connection]}
value={CONNECTION_LABEL[connection]}
/>
<Badge
class="ui-agent-term-preview-expand"
variant="quiet"
@ -190,9 +193,19 @@ export function AgentTermPreview({
title={`open ${paths.label}'s terminal in its own tab`}
onClick={() => openTab({ href: paths.page, label: paths.label })}
/>
)}
</div>
</div>
)}
<div class="terminal-wrap ui-agent-term-preview-wrap">
{/* Full-tab mount only: the connection badge floats in the
terminal's top-left corner (the other badges own the
top-right), instead of sitting in a toolbar row above it. */}
{fullHeight && (
<Badge
class="ui-agent-term-preview-status ui-agent-term-preview-live"
tone={CONNECTION_TONE[connection]}
value={CONNECTION_LABEL[connection]}
/>
)}
{header && showHeaderBadges && (
<div class="ui-agent-term-preview-header-badges">
<Badge

View file

@ -1,26 +1,13 @@
/* <AgentTerminalPage> — gives `AgentTermPreview`'s `fullHeight` flex
chain (`.ui-agent-term-preview-full`, AgentTermPreview.css) something
real to fill. `Shell.css`'s `.shell-body` has no fixed height of its
own (it's a plain flow column sized by content), so without an
explicit height here the chain's `flex: 1` would resolve against
nothing and the terminal would collapse to its content's own height
instead of filling the viewport — the opposite of what "full-height"
mount means for this page specifically (the 12em-capped preview
inside `AgentsPage` deliberately has no such wrapper; this is the one
place that needs it).
`100dvh`, not `100vh` — `100vh` on a mobile browser includes the
address-bar chrome even while it's visibly on-screen, so the terminal
would render taller than the actually-visible viewport there;
`100dvh` tracks the real visible area instead. The subtraction below
is a fixed estimate (Shell.css's own `.shell-header` padding +
`.shell-body`'s), not a measured value — an exact fit isn't worth a
ResizeObserver for a single full-bleed page; a few pixels short of
the viewport edge reads as intentional breathing room, not a bug. */
/* <AgentTerminalPage> — fills `.shell-body-full` (Shell.css), which is
already everything below the header bar, full width and height. This
wrapper only has to pass that height down so `AgentTermPreview`'s
`fullHeight` flex chain (`.ui-agent-term-preview-full`) has a real box
to fill. */
.ui-agent-terminal-page {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
height: calc(100dvh - 4.5em - 3em);
}
.ui-agent-terminal-page .ui-agent-term-preview-full {

View file

@ -242,6 +242,27 @@
.shell-body-wide {
max-width: 90em;
}
/* Full-bleed route (the agent terminal): the shell becomes a viewport-high
column, the header keeps its natural height, and the body takes every
remaining pixel with no width cap or padding. `100dvh`, not `100vh` —
`vh` includes mobile browser chrome that is on screen. */
.shell-full {
display: flex;
flex-direction: column;
height: 100dvh;
}
.shell-full .shell-header {
flex-shrink: 0;
}
.shell-body-full {
flex: 1;
min-height: 0;
max-width: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
@keyframes shell-page-enter {
from {
opacity: 0;

View file

@ -63,19 +63,19 @@ const NAV_ITEMS: { href: string; label: string; accent: string }[] = [
const WIDE_BODY_ROUTES = new Set(["/issues", "/agents"]);
// `/agents/:name/terminal` (and a subagent's, under
// `/agents/:name/subagents/:subagent/`) needs the same widening but can't join
// the `Set` above — it's a parameterized path, one literal string can't
// match every agent name. A 60em-capped terminal pane reads as cramped
// (this is a full tab, not a sidebar preview), same reasoning as the
// table routes above just for a different reason (a wide pane, not a
// wide table).
const WIDE_BODY_ROUTE_PATTERN =
// `/agents/:name/subagents/:subagent/`) is full-bleed: the terminal fills
// the whole viewport below the header bar, edge to edge (`shell-full` /
// `shell-body-full` in Shell.css). It can't join the `Set` above — it's a
// parameterized path, one literal string can't match every agent name.
const FULL_BLEED_ROUTE_PATTERN =
/^\/agents\/[^/]+(\/subagents\/[^/]+)?\/terminal$/;
function isFullBleedRoute(location: string): boolean {
return FULL_BLEED_ROUTE_PATTERN.test(location);
}
function isWideBodyRoute(location: string): boolean {
return (
WIDE_BODY_ROUTES.has(location) || WIDE_BODY_ROUTE_PATTERN.test(location)
);
return WIDE_BODY_ROUTES.has(location);
}
// Static fallback — matches `index.html`'s `<title>` default, so a page
@ -360,7 +360,7 @@ export function Shell({ children }: { children: ComponentChildren }) {
const navAccent = indicator?.accent ?? "var(--purple)";
return (
<div class="shell">
<div class={"shell" + (isFullBleedRoute(location) ? " shell-full" : "")}>
<header class="shell-header">
<span
class="shell-brand"
@ -447,7 +447,9 @@ export function Shell({ children }: { children: ComponentChildren }) {
apply, only the max-width cap changes. */}
<div
class={
"shell-body" + (isWideBodyRoute(location) ? " shell-body-wide" : "")
"shell-body" +
(isWideBodyRoute(location) ? " shell-body-wide" : "") +
(isFullBleedRoute(location) ? " shell-body-full" : "")
}
key={location}
>