swarm-ui: fold the agent terminal into AgentsPage's detail panel, not a route
mara's steer on the issue, right as the previous commit's plan was posted: the swarm-level term isn't a separate page, it's part of the same pwa -- selecting an agent should show a small preview (no header, no input) below the existing detail fields. That's the whole MVP for this issue; the full terminal (+ sending input back to the agent) is explicit follow-up scope. AgentTermPage -> AgentTermPreview: drops the Panel wrapper, the "terminal" nav badge + wouter route, and the back-link -- just the inline preview, height-capped at 12em (@hive/shared/terminal.css's own default is a much taller 72vh/60em, sized for a dedicated page rather than a peek).
This commit is contained in:
parent
593923375c
commit
727bfb775b
5 changed files with 42 additions and 64 deletions
|
|
@ -3,7 +3,6 @@
|
||||||
import { Route, Switch } from "wouter-preact";
|
import { Route, Switch } from "wouter-preact";
|
||||||
import { Shell } from "./shell/Shell.js";
|
import { Shell } from "./shell/Shell.js";
|
||||||
import { AgentsPage } from "./pages/agents/AgentsPage.js";
|
import { AgentsPage } from "./pages/agents/AgentsPage.js";
|
||||||
import { AgentTermPage } from "./pages/agents/AgentTermPage.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 { HivesPage } from "./pages/HivesPage.js";
|
||||||
|
|
@ -24,7 +23,6 @@ export function App() {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={HivesPage} />
|
<Route path="/" component={HivesPage} />
|
||||||
<Route path="/agents" component={AgentsPage} />
|
<Route path="/agents" component={AgentsPage} />
|
||||||
<Route path="/agents/:name/term" component={AgentTermPage} />
|
|
||||||
<Route path="/jobs" component={JobsPage} />
|
<Route path="/jobs" component={JobsPage} />
|
||||||
<Route path="/issues" component={IssueReportPage} />
|
<Route path="/issues" component={IssueReportPage} />
|
||||||
<Route path="/components" component={ComponentsPage} />
|
<Route path="/components" component={ComponentsPage} />
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
/* <AgentTermPage> — `.terminal-wrap`/`.live.terminal` come from
|
|
||||||
@hive/shared/terminal.css unstyled (its own default 32em max-height
|
|
||||||
+ scroll is exactly right here, no page override needed). Just
|
|
||||||
spacing for the status badge + back link this page adds around it. */
|
|
||||||
@import "@hive/shared/terminal.css";
|
|
||||||
|
|
||||||
.ui-agent-term-status {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-block-end: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-agent-term-back {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--fg);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-agent-term-back:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* <AgentTermPreview> — a *preview*, not the full terminal: a much
|
||||||
|
shorter cap than @hive/shared/terminal.css's own 32em default so it
|
||||||
|
reads as "peek at what's happening" inside the detail panel rather
|
||||||
|
than competing with the panel's own fields for vertical space. */
|
||||||
|
@import "@hive/shared/terminal.css";
|
||||||
|
|
||||||
|
.ui-agent-term-preview {
|
||||||
|
margin-block-start: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-agent-term-preview-status {
|
||||||
|
margin-block-end: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-agent-term-preview-wrap.terminal-wrap {
|
||||||
|
max-height: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-agent-term-preview-wrap .live.terminal {
|
||||||
|
max-height: 12em;
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
// <AgentTermPage> — read-only live terminal for one agent, consuming
|
// <AgentTermPreview> — the MVP mara's own ruling asked for: not a separate
|
||||||
// swarm-controller's `GET /api/agents/{name}/term/stream`. First
|
// page, a small read-only preview embedded below `AgentsPage`'s detail
|
||||||
// per-agent detail route in swarm-ui (linked from `AgentsPage`'s detail
|
// panel fields. No header, no input — those (plus sending input back
|
||||||
// panel). Live-only, no backfill/history — see `useSwarmTermStream`'s
|
// to the agent) are explicitly follow-up scope, not this issue's.
|
||||||
// doc for why a fresh visit starts empty rather than replaying anything.
|
// Consumes swarm-controller's `GET /api/agents/{name}/term/stream` via
|
||||||
//
|
// `useSwarmTermStream`, rendering through the same `@hive/shared`
|
||||||
// Auto-scroll is the simple "stick to bottom unless the operator
|
// `Row` component the per-hive local terminal uses — "share components
|
||||||
// scrolled up" rule `@hive/agent`'s `LiveStream` uses, minus its
|
// between hive and swarm level term as much as possible" (mara).
|
||||||
// load-more/history plumbing (nothing to load older here).
|
|
||||||
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||||
import { Link, useParams } from "wouter-preact";
|
|
||||||
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
||||||
import { Row } from "@hive/shared/term-row.js";
|
import { Row } from "@hive/shared/term-row.js";
|
||||||
import { Panel } from "../../ui/panel/Panel.js";
|
|
||||||
import { useSwarmTermStream, type ConnectionState } from "./useSwarmTermStream.js";
|
import { useSwarmTermStream, type ConnectionState } from "./useSwarmTermStream.js";
|
||||||
import "./AgentTermPage.css";
|
import "./AgentTermPreview.css";
|
||||||
|
|
||||||
const NEAR_BOTTOM_PX = 48;
|
const NEAR_BOTTOM_PX = 48;
|
||||||
|
|
||||||
|
|
@ -31,10 +28,9 @@ const CONNECTION_TONE: Record<ConnectionState, BadgeTone> = {
|
||||||
closed: "negative",
|
closed: "negative",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AgentTermPage() {
|
export function AgentTermPreview({ agentName }: { agentName: string }) {
|
||||||
const { name } = useParams<{ name: string }>();
|
|
||||||
const { rows, connection } = useSwarmTermStream(
|
const { rows, connection } = useSwarmTermStream(
|
||||||
`/api/agents/${encodeURIComponent(name ?? "")}/term/stream`,
|
`/api/agents/${encodeURIComponent(agentName)}/term/stream`,
|
||||||
);
|
);
|
||||||
const logRef = useRef<HTMLDivElement>(null);
|
const logRef = useRef<HTMLDivElement>(null);
|
||||||
const [stickToBottom, setStickToBottom] = useState(true);
|
const [stickToBottom, setStickToBottom] = useState(true);
|
||||||
|
|
@ -48,29 +44,20 @@ export function AgentTermPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs after Preact has committed new rows, so `scrollHeight` is
|
// Runs after Preact has committed new rows, so `scrollHeight` is
|
||||||
// already current — same ordering `LiveStream`'s own layout effect
|
// already current — same ordering `@hive/agent`'s `LiveStream` uses.
|
||||||
// relies on.
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const el = logRef.current;
|
const el = logRef.current;
|
||||||
if (el && stickToBottom) el.scrollTop = el.scrollHeight - el.clientHeight;
|
if (el && stickToBottom) el.scrollTop = el.scrollHeight - el.clientHeight;
|
||||||
}, [rows, stickToBottom]);
|
}, [rows, stickToBottom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<div class="ui-agent-term-preview">
|
||||||
title={`terminal — ${name}`}
|
|
||||||
icon="🖥️"
|
|
||||||
actions={
|
|
||||||
<Link to="/agents" class="ui-agent-term-back">
|
|
||||||
← agents
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Badge
|
<Badge
|
||||||
class="ui-agent-term-status"
|
class="ui-agent-term-preview-status"
|
||||||
tone={CONNECTION_TONE[connection]}
|
tone={CONNECTION_TONE[connection]}
|
||||||
value={CONNECTION_LABEL[connection]}
|
value={CONNECTION_LABEL[connection]}
|
||||||
/>
|
/>
|
||||||
<div class="terminal-wrap">
|
<div class="terminal-wrap ui-agent-term-preview-wrap">
|
||||||
<div class="live terminal" ref={logRef} onScroll={handleScroll}>
|
<div class="live terminal" ref={logRef} onScroll={handleScroll}>
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
<div class="row note">
|
<div class="row note">
|
||||||
|
|
@ -83,6 +70,6 @@ export function AgentTermPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -26,12 +26,12 @@
|
||||||
// see its own comment) — `viewMode` here just says which one to show.
|
// see its own comment) — `viewMode` here just says which one to show.
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import type { ComponentChildren } from "preact";
|
import type { ComponentChildren } from "preact";
|
||||||
import { useLocation } from "wouter-preact";
|
|
||||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||||
import { Badge } from "@hive/shared/badge.js";
|
import { Badge } from "@hive/shared/badge.js";
|
||||||
import { LinkIcon } from "@hive/shared/icons.js";
|
import { LinkIcon } from "@hive/shared/icons.js";
|
||||||
import { AgentCard } from "./AgentCard.js";
|
import { AgentCard } from "./AgentCard.js";
|
||||||
|
import { AgentTermPreview } from "./AgentTermPreview.js";
|
||||||
import { FRESHNESS, type AgentRow } from "./AgentTypes.js";
|
import { FRESHNESS, type AgentRow } from "./AgentTypes.js";
|
||||||
import { Button } from "../../ui/button/Button.js";
|
import { Button } from "../../ui/button/Button.js";
|
||||||
import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
|
import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
|
||||||
|
|
@ -96,7 +96,6 @@ type ViewMode = "cards" | "table";
|
||||||
const VIEW_MODE_KEY = "swarm-ui:agents:view-mode";
|
const VIEW_MODE_KEY = "swarm-ui:agents:view-mode";
|
||||||
|
|
||||||
export function AgentsPage() {
|
export function AgentsPage() {
|
||||||
const [, navigate] = useLocation();
|
|
||||||
const [rows, setRows] = useState<AgentRow[] | null>(null);
|
const [rows, setRows] = useState<AgentRow[] | null>(null);
|
||||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||||
const [intervalMs, setIntervalMs] =
|
const [intervalMs, setIntervalMs] =
|
||||||
|
|
@ -566,17 +565,6 @@ export function AgentsPage() {
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<div class="ui-agent-detail-actions">
|
<div class="ui-agent-detail-actions">
|
||||||
<Badge
|
|
||||||
variant="quiet"
|
|
||||||
icon="🖥️"
|
|
||||||
value="terminal"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/agents/${encodeURIComponent(detailTarget.name)}/term`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
title={`view ${detailTarget.name}'s live terminal`}
|
|
||||||
/>
|
|
||||||
<Badge
|
<Badge
|
||||||
variant="quiet"
|
variant="quiet"
|
||||||
icon={<LinkIcon />}
|
icon={<LinkIcon />}
|
||||||
|
|
@ -594,6 +582,10 @@ export function AgentsPage() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* MVP scope per mara's own ruling: a small read-only
|
||||||
|
preview, no header/no input — the full terminal
|
||||||
|
(+ sending input back) is explicit follow-up scope. */}
|
||||||
|
<AgentTermPreview agentName={detailTarget.name} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p class="ui-agents-empty">select an agent to see its details</p>
|
<p class="ui-agents-empty">select an agent to see its details</p>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue