Compare commits
28 changed files with 633 additions and 588 deletions
|
|
@ -23,7 +23,7 @@ shut. A dropdown inside a dialog opens above the dialog and the dialog doesn't c
|
|||
| route | what |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `/` | the start page: the swarm's name over a digital-rain backdrop, plus a paperclip offering to set up an agent while no `ruth` exists, or her hive declares her destroyed (it opens the create form with `ruth` filled in); the swarm name in the header links here |
|
||||
| `/agents` | every agent as a card that opens in place (status, config PR, wanted state, inference settings, resource caps, permissions, accounts, terminal), under a folded **hives** card with the hive directory and each hive's last reported status; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
|
||||
| `/agents` | every agent, under a folded **hives** card with the hive directory and each hive's last reported status: status, config PR, wanted state, inference settings, resource caps, permissions; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
|
||||
| `/agents/<name>/terminal` | one agent's live terminal |
|
||||
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
||||
| `/reports` | two folded cards, each read only once opened: the **alerts** the swarm's Grafana holds now, the firing ones, with a button for the pending, no-data and error ones and a link to Grafana's own alert list; read through the controller with a Viewer service-account token, and a cross-repo **issue report** |
|
||||
|
|
@ -53,31 +53,9 @@ the code back. The credentials go to the agent through the secret store
|
|||
([`claude-login.md`](claude-login.md)). The agent's own web UI login still
|
||||
works too.
|
||||
|
||||
### The agents page
|
||||
|
||||
`/agents` is the hives card, then one column of cards, one per agent. A card's
|
||||
header shows its icon, name, freshness, the status message and the **wanted**
|
||||
menu (up, offline, paused, destroy), and a **login needed** badge with a
|
||||
**log in** button when its claude has no session. Select the header to open the
|
||||
card in place; select it again to close it. Several cards can be open at once,
|
||||
and a card stays open across the page's refresh, but not across a reload. The
|
||||
filter bar above the cards filters by name, hive, status, message, wanted state
|
||||
and config PR.
|
||||
|
||||
An opened card has four sections, each folded except **overview**, and each
|
||||
reads nothing until you open it:
|
||||
|
||||
- **overview**: the agent's turn state, context and cost, its hive, and its open
|
||||
config PR.
|
||||
- **settings**: inference, instructions, resource caps, permissions and allowed
|
||||
domains, each with its own editor (described below).
|
||||
- **accounts**: the agent's linked accounts, and the menu that links another.
|
||||
- **terminal**: the live terminal with its message box, and the agent's
|
||||
subagents under it (next section).
|
||||
|
||||
### Agent and subagent terminals
|
||||
|
||||
An opened agent card on `/agents` shows, in its **terminal** section, a small live preview of its
|
||||
An agent's detail panel on `/agents` shows a small live preview of its
|
||||
terminal, and below it the agent's **subagents**: every subagent the swarm
|
||||
queue holds terminal rows for from the last 24 hours. Picking one shows its
|
||||
terminal in the same preview, and **expand** opens it in its own tab, as it
|
||||
|
|
@ -135,7 +113,7 @@ with no icon keeps the hyperhive mark.
|
|||
The tab also has an agent badge, with the agent's icon and name, ahead of the
|
||||
connection badge. It opens the agent's details in a popover under the badge:
|
||||
the same status, state, wanted state, inference settings, resource caps, permissions, allowed domains and linked accounts as the
|
||||
opened card on `/agents`, without the terminal preview. The popover doesn't dim the
|
||||
panel on `/agents`, without the terminal preview. The popover doesn't dim the
|
||||
page. Selecting outside it, or Escape, closes it. From the keyboard, opening a
|
||||
popover moves focus into it, and Escape puts focus back on the badge. The filter
|
||||
popovers on table headers work the same way. You can stop or pause the
|
||||
|
|
@ -558,7 +536,7 @@ text stays.
|
|||
|
||||
### Inference settings
|
||||
|
||||
An opened agent card shows, in its **settings** section, its model and effort under **inference**, each
|
||||
The agent's detail panel shows its model and effort under **inference**, each
|
||||
with where it comes from: the swarm, its agent.nix, its own pick, or the
|
||||
default. Below them are the subagent model list the swarm sets, or "its own"
|
||||
when it sets none, and the subagent cap, which reads "no cap" when unset.
|
||||
|
|
@ -566,13 +544,13 @@ when it sets none, and the subagent cap, which reads "no cap" when unset.
|
|||
stores; saving does a `PUT /api/agents/{agent}/inference` of
|
||||
the whole value. A field you leave empty stays out of it, so the agent keeps
|
||||
its own value for that field; an empty max subagents is no cap. **clear the swarm's setting** sends the `DELETE`.
|
||||
The card reads `GET /api/agents/{agent}/inference`; before the agent's first
|
||||
The panel reads `GET /api/agents/{agent}/inference`; before the agent's first
|
||||
report reaches the controller, it shows the swarm's stored values only.
|
||||
[inference](inference.md) describes the settings themselves.
|
||||
|
||||
### Editing an agent's instructions
|
||||
|
||||
The **instructions** row of an opened agent card's **settings** has an **edit** button. It
|
||||
The **instructions** row of an agent's detail panel has an **edit** button. It
|
||||
opens `AGENTS.md`, the file the agent reads its instructions from, in a plain
|
||||
text box. The text is the copy the agent last published, so the box still
|
||||
opens for a stopped agent, but **save** needs the agent running.
|
||||
|
|
@ -588,7 +566,7 @@ dialog keeps an unsaved edit for the next time you open it.
|
|||
|
||||
### Resource caps
|
||||
|
||||
An opened agent card shows, under **settings**, its CPU quota and memory max under **resource
|
||||
The agent's detail panel shows its CPU quota and memory max under **resource
|
||||
caps**. A cap is the swarm's, so it follows the agent to whichever hive runs
|
||||
it, and it replaces what the hive says for that field. A field the swarm
|
||||
leaves out shows as the hive deciding it. **edit** opens a dialog with the two
|
||||
|
|
@ -623,14 +601,14 @@ to an agent's traffic from then on.
|
|||
|
||||
A saved change isn't instant. The hive reads the caps about every 30 seconds,
|
||||
and a running agent takes a changed cap at its next spawn or rebuild, so the
|
||||
row shows the new value before the agent runs under it. The card reads
|
||||
`GET /api/agents/{agent}/limits`; when that read fails, the card shows the
|
||||
row shows the new value before the agent runs under it. The panel reads
|
||||
`GET /api/agents/{agent}/limits`; when that read fails, the panel shows the
|
||||
reason and a **retry** button. [hivectl](../tools/hivectl.md#per-agent-resource-limits)
|
||||
sets the hive's own limits.
|
||||
|
||||
### Permissions
|
||||
|
||||
An opened agent card shows, under **settings**, what the swarm grants it under **permissions**:
|
||||
The agent's detail panel shows what the swarm grants it under **permissions**:
|
||||
the default set, none, or a count with one badge per permission. A swarm
|
||||
grant is the agent's whole set, so it follows the agent to whichever hive runs
|
||||
it. **edit** opens a dialog with one checkbox per permission the controller
|
||||
|
|
@ -647,14 +625,14 @@ them.
|
|||
|
||||
A saved change isn't instant. The hive reads the grant about every 30 seconds
|
||||
and the agent takes it at its next turn. A change to `manage_root_agent`
|
||||
restarts the agent after its current turn. The card reads
|
||||
`GET /api/agents/{agent}/permissions`; when that read fails, the card shows
|
||||
restarts the agent after its current turn. The panel reads
|
||||
`GET /api/agents/{agent}/permissions`; when that read fails, the panel shows
|
||||
the reason and a **retry** button. `swarmctl agent permissions set` sets the
|
||||
same grant from a shell.
|
||||
|
||||
### Allowed domains
|
||||
|
||||
An opened agent card lists, under **settings**, the destinations it may reach through its
|
||||
The agent's detail panel lists the destinations it may reach through its
|
||||
hive's egress proxy under **allowed domains**, beyond the base list every
|
||||
agent has: the entries for every agent in the swarm (badge **swarm-wide**) and
|
||||
the ones for this agent alone (badge with the agent's name). An entry matters
|
||||
|
|
@ -719,7 +697,7 @@ the dialog goes back to step 2 and shows the reason there.
|
|||
### Settings at creation
|
||||
|
||||
Step 4 of the **+ agent** dialog has **resource caps** and **inference**, with
|
||||
the same fields as the opened card's settings editors. Leave the
|
||||
the same fields as the detail panel's editors. Leave the
|
||||
fields empty and the request is the plain name and hive. Fill them and `POST /api/agents` carries them
|
||||
as optional `limits` and `inference` objects, the same shapes the two `PUT`
|
||||
routes take. The controller checks them with the `PUT` rules before it queues
|
||||
|
|
@ -759,13 +737,13 @@ name that already holds an account; delete that account first to replace it.
|
|||
The fourth, the API-key environment, is the exception: a save overwrites what the
|
||||
store holds.
|
||||
|
||||
An opened agent card lists its linked accounts in its **accounts** section, one row
|
||||
The agent's detail panel lists its linked accounts under **accounts**, one row
|
||||
per account: kind, name and host. Opening an agent makes one request,
|
||||
`GET /api/hives/{hive}/agents/{agent}/linked-accounts`, which returns every
|
||||
account of that agent as names and hosts and never a credential. When the card
|
||||
account of that agent as names and hosts and never a credential. When the panel
|
||||
fails to read the accounts, the inference settings, the resource caps or the
|
||||
permissions, it
|
||||
shows the reason as text. A list the card already loaded stays on screen
|
||||
shows the reason as text. A list the panel already loaded stays on screen
|
||||
beneath the error.
|
||||
|
||||
| kind | one row per | name | host |
|
||||
|
|
@ -775,11 +753,11 @@ beneath the error.
|
|||
| github | `swarm/agents/<agent>/github-token`, if any | `github` | `github.com` |
|
||||
| `backend` | `swarm/agents/<agent>/backend-env`, if any | `backend-env` | none |
|
||||
|
||||
The route names the second kind `forgejo`; the card shows it as **forge**,
|
||||
The route names the second kind `forgejo`; the panel shows it as **forge**,
|
||||
the word the link menu uses.
|
||||
|
||||
The matrix `main` row is the agent's own account, which the swarm mints;
|
||||
it carries an **own account** badge. The card requests the list again when
|
||||
it carries an **own account** badge. The panel requests the list again when
|
||||
a link dialog closes.
|
||||
|
||||
- **link a matrix account** — `PUT /api/hives/{hive}/agents/{agent}/matrix-accounts/{account}`,
|
||||
|
|
@ -827,7 +805,7 @@ request:
|
|||
|
||||
swarm-controller removes every version of the entry from the swarm secret
|
||||
store, and answers 404 when the store holds nothing there. It refuses `main`: the
|
||||
swarm mints that account and would re-mint it. The card requests the list
|
||||
swarm mints that account and would re-mint it. The panel requests the list
|
||||
again after a delete, including a 404 one: the account being already gone is
|
||||
the end state the delete wanted, so the dialog closes without an error.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,15 +8,23 @@
|
|||
// for free, so this hand-rolls the same "outside click or Escape
|
||||
// closes" contract `Dialog` gets from the platform.
|
||||
//
|
||||
// Positioning is the caller's job: render `<Dropdown>` as a child of a
|
||||
// `position: relative` wrapper (the badge + dropdown pair) and it anchors to
|
||||
// that box's bottom-left via CSS. Put the wrapper outside any scrolling
|
||||
// container: an ancestor with `overflow-x: auto` computes `overflow-y` to `auto`
|
||||
// too (per the CSS overflow spec), so it clips the dropdown vertically. A
|
||||
// `position: fixed` list rendered through a portal escapes that, as
|
||||
// `../../ui/table/Table.tsx` does for its column filters; this component had
|
||||
// that mode for the agents table, and dropped it with the table.
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
// Positioning is the caller's job by default: render `<Dropdown>` as a
|
||||
// child of a `position: relative` wrapper (the badge + dropdown pair)
|
||||
// and it anchors to that box's bottom-left via CSS — no portal, no
|
||||
// re-parenting to `<body>` to reason about. That's wrong the moment the
|
||||
// wrapper sits inside a scrolling container, though: an ancestor with
|
||||
// `overflow-x: auto` computes `overflow-y` to `auto` too (per the CSS
|
||||
// overflow spec), so it clips the dropdown vertically the instant the
|
||||
// container is shorter than header-plus-dropdown — reported against
|
||||
// swarm-ui's agent table (a `WantedMenu` badge two rows from the
|
||||
// bottom). `../../ui/table/Table.tsx` hit the identical clip for its
|
||||
// own column-filter popover and fixed it with `position: fixed`
|
||||
// computed from the anchor's `getBoundingClientRect()`, rendered via a
|
||||
// portal outside the scrolling subtree; the optional `portal` prop
|
||||
// below opts a caller into that same recipe instead of a second
|
||||
// hand-rolled copy of it.
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { createPortal } from "preact/compat";
|
||||
import type { ComponentChildren, RefObject } from "preact";
|
||||
import "./Dropdown.css";
|
||||
|
||||
|
|
@ -53,6 +61,13 @@ export interface DropdownProps {
|
|||
* one ref) already avoids this.
|
||||
*/
|
||||
anchorRef?: RefObject<HTMLElement>;
|
||||
/**
|
||||
* Escape a clipping scroll ancestor by portal-rendering to `<body>`
|
||||
* with `position: fixed`, computed from `anchorRef`'s rect — see the
|
||||
* file-top comment. Requires `anchorRef`; a caller in an unscrolled,
|
||||
* un-clipped context (the common case) can omit this entirely.
|
||||
*/
|
||||
portal?: boolean;
|
||||
}
|
||||
|
||||
export function Dropdown({
|
||||
|
|
@ -63,8 +78,14 @@ export function Dropdown({
|
|||
onClose,
|
||||
label,
|
||||
anchorRef,
|
||||
portal,
|
||||
}: DropdownProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// Only populated (and only consulted) in `portal` mode — see the
|
||||
// effect below and the file-top comment for why fixed-position
|
||||
// coordinates are needed at all here.
|
||||
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handlePointerDown(e: PointerEvent) {
|
||||
|
|
@ -87,10 +108,35 @@ export function Dropdown({
|
|||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
// Same recipe as `Table.tsx`'s column-filter popover: compute once on
|
||||
// open, then keep it pinned to the anchor across any scroll in the
|
||||
// tree (`capture: true` sees a nested scroll container too, since
|
||||
// plain `scroll` doesn't bubble) or a viewport resize.
|
||||
useEffect(() => {
|
||||
if (!open || !portal) return;
|
||||
function recompute() {
|
||||
const rect = anchorRef?.current?.getBoundingClientRect();
|
||||
if (rect) setPos({ top: rect.bottom + 4, left: rect.left });
|
||||
}
|
||||
recompute();
|
||||
window.addEventListener("scroll", recompute, true);
|
||||
window.addEventListener("resize", recompute);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", recompute, true);
|
||||
window.removeEventListener("resize", recompute);
|
||||
};
|
||||
}, [open, portal, anchorRef]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div class="ui-dropdown" role="menu" aria-label={label} ref={ref}>
|
||||
const list = (
|
||||
<div
|
||||
class="ui-dropdown"
|
||||
role="menu"
|
||||
aria-label={label}
|
||||
ref={ref}
|
||||
style={portal && pos ? { position: "fixed", ...pos } : undefined}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -112,4 +158,10 @@ export function Dropdown({
|
|||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Not yet positioned (first render after opening, before the effect
|
||||
// above runs) — skip the portal render entirely rather than flashing
|
||||
// the list at `document.body`'s origin for one frame.
|
||||
if (portal) return pos ? createPortal(list, document.body) : null;
|
||||
return list;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,3 @@
|
|||
align-items: center;
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
||||
/* The folded sections of an opened card sit one under another, each with the
|
||||
shared disclosure look; the fields keep the grid above. */
|
||||
.ui-agent-detail-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
// <AgentDetail> — one agent's details: its status, wanted state, hive, config
|
||||
// PR, inference settings, resource caps and linked accounts, plus the actions on
|
||||
// them. Two layouts. In an opened card on `/agents` (`inCard`) the card header
|
||||
// already shows status, message and wanted state, so the body is four folded
|
||||
// sections (`sections.ts`): overview, settings, accounts, and the terminal with
|
||||
// its subagents, each built when first opened. In the detail dialog on a terminal
|
||||
// page it is one flat list with those three rows, and no terminal, since the
|
||||
// page is already beside one.
|
||||
// PR, inference settings, resource caps and linked accounts, plus the actions on them. The
|
||||
// body of the detail panel on `/agents` (where `withTerminal` adds the live
|
||||
// terminal and subagents below it) and of the detail dialog on a terminal
|
||||
// page (which is already beside the terminal, so omits it).
|
||||
//
|
||||
// Takes the agent's *live* row and the shared `AgentActions`, so a state
|
||||
// change or a link made here shows wherever else the same row is drawn.
|
||||
|
|
@ -23,262 +20,140 @@ import { AgentTermPreview } from "../terminal/AgentTermPreview.js";
|
|||
import { FRESHNESS, type AgentRow } from "../AgentTypes.js";
|
||||
import { LinkAccountMenu } from "./LinkAccountMenu.js";
|
||||
import { LinkedAccounts } from "./LinkedAccounts.js";
|
||||
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
|
||||
import {
|
||||
SECTION_TITLE,
|
||||
sectionsFor,
|
||||
startsOpen,
|
||||
type DetailSection,
|
||||
} from "./sections.js";
|
||||
import { SubagentTerms } from "../terminal/SubagentTerms.js";
|
||||
import { WantedMenu } from "../roster/WantedMenu.js";
|
||||
import "./AgentDetail.css";
|
||||
|
||||
/** The rows the card header already shows: status, message and wanted. */
|
||||
function HeaderRows({
|
||||
row,
|
||||
actions,
|
||||
}: {
|
||||
row: AgentRow;
|
||||
actions: AgentActions;
|
||||
}) {
|
||||
const { tone, label } = FRESHNESS[row.freshness];
|
||||
const declareError = actions.actionErrors.get(row.name);
|
||||
return (
|
||||
<>
|
||||
<dt>status</dt>
|
||||
<dd>
|
||||
<Badge
|
||||
tone={tone}
|
||||
value={
|
||||
<>
|
||||
{label}
|
||||
{row.last_seen_unix !== null ? (
|
||||
<>
|
||||
{" "}
|
||||
(<RelativeTime epochMs={row.last_seen_unix * 1000} />)
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</dd>
|
||||
<dt>message</dt>
|
||||
<dd>{row.snapshot?.status_text ?? "—"}</dd>
|
||||
<dt>wanted</dt>
|
||||
<dd>
|
||||
{/* Full menu (destroy included) — mara: "destroy is already
|
||||
available via wanted state", no separate button needed. */}
|
||||
<WantedMenu
|
||||
row={row}
|
||||
pending={actions.pendingAgents.has(row.name)}
|
||||
onSelectUp={actions.selectUp}
|
||||
onSelectOffline={actions.selectOffline}
|
||||
onSelectPaused={actions.selectPaused}
|
||||
onDestroy={actions.requestDestroy}
|
||||
/>
|
||||
{/* A failed declaration shows here too: on a terminal page's
|
||||
dialog there is no roster card to carry the "failed" badge. */}
|
||||
{declareError ? (
|
||||
<ApiErrorPanel
|
||||
context="the declaration failed"
|
||||
problem={declareError}
|
||||
/>
|
||||
) : null}
|
||||
</dd>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewRows({ row }: { row: AgentRow }) {
|
||||
return (
|
||||
<>
|
||||
<dt>state</dt>
|
||||
<dd>
|
||||
<AgentStateRow key={row.name} agent={row.name} />
|
||||
</dd>
|
||||
<dt>hive</dt>
|
||||
<dd>{row.hive ?? "—"}</dd>
|
||||
<dt>config PR</dt>
|
||||
<dd>
|
||||
{row.config_pr ? (
|
||||
<Badge
|
||||
tone="warning"
|
||||
value={
|
||||
row.config_pr.html_url ? (
|
||||
<a
|
||||
href={row.config_pr.html_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
#{row.config_pr.pr_number}
|
||||
</a>
|
||||
) : (
|
||||
`#${row.config_pr.pr_number}`
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</dd>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsRows({ agent }: { agent: string }) {
|
||||
return (
|
||||
<>
|
||||
<dt>inference</dt>
|
||||
<dd>
|
||||
<AgentInference key={agent} agent={agent} />
|
||||
</dd>
|
||||
<dt>instructions</dt>
|
||||
<dd>
|
||||
<AgentInstructions key={agent} agent={agent} />
|
||||
</dd>
|
||||
<dt>resource caps</dt>
|
||||
<dd>
|
||||
<AgentLimits key={agent} agent={agent} />
|
||||
</dd>
|
||||
<dt>permissions</dt>
|
||||
<dd>
|
||||
<AgentPermissions key={agent} agent={agent} />
|
||||
</dd>
|
||||
<dt>allowed domains</dt>
|
||||
<dd>
|
||||
<AllowedDomains key={agent} agent={agent} />
|
||||
</dd>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountsRows({
|
||||
row,
|
||||
actions,
|
||||
}: {
|
||||
row: AgentRow;
|
||||
actions: AgentActions;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<dt>accounts</dt>
|
||||
<dd>
|
||||
{row.hive ? (
|
||||
<LinkedAccounts
|
||||
key={`${row.hive}/${row.name}`}
|
||||
hive={row.hive}
|
||||
agent={row.name}
|
||||
version={actions.linkVersion}
|
||||
/>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</dd>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkAccountAction({
|
||||
row,
|
||||
actions,
|
||||
}: {
|
||||
row: AgentRow;
|
||||
actions: AgentActions;
|
||||
}) {
|
||||
return (
|
||||
<div class="ui-agent-detail-actions">
|
||||
<LinkAccountMenu
|
||||
agent={row.name}
|
||||
hasHive={row.hive !== null}
|
||||
onSelect={(kind) => actions.openLink(row, kind)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** The body of one folded section of an opened card. */
|
||||
function SectionBody({
|
||||
section,
|
||||
row,
|
||||
actions,
|
||||
}: {
|
||||
section: DetailSection;
|
||||
row: AgentRow;
|
||||
actions: AgentActions;
|
||||
}) {
|
||||
switch (section) {
|
||||
case "overview":
|
||||
return (
|
||||
<dl class="ui-agent-detail-fields">
|
||||
<OverviewRows row={row} />
|
||||
</dl>
|
||||
);
|
||||
case "settings":
|
||||
return (
|
||||
<dl class="ui-agent-detail-fields">
|
||||
<SettingsRows agent={row.name} />
|
||||
</dl>
|
||||
);
|
||||
case "accounts":
|
||||
return (
|
||||
<>
|
||||
<dl class="ui-agent-detail-fields">
|
||||
<AccountsRows row={row} actions={actions} />
|
||||
</dl>
|
||||
<LinkAccountAction row={row} actions={actions} />
|
||||
</>
|
||||
);
|
||||
case "terminal":
|
||||
return (
|
||||
<>
|
||||
{/* The agent's live terminal and its input box. */}
|
||||
<AgentTermPreview agentName={row.name} />
|
||||
<SubagentTerms key={row.name} agentName={row.name} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function AgentDetail({
|
||||
row,
|
||||
actions,
|
||||
withTerminal = false,
|
||||
inCard = false,
|
||||
}: {
|
||||
row: AgentRow;
|
||||
actions: AgentActions;
|
||||
/** Add the live terminal and subagents: for the flat layout, below the rows. */
|
||||
withTerminal?: boolean;
|
||||
/** The folded sections of an opened card, without the rows its header shows. */
|
||||
inCard?: boolean;
|
||||
}) {
|
||||
if (inCard) {
|
||||
return (
|
||||
<div class="ui-agent-detail ui-agent-detail-sections">
|
||||
{sectionsFor(withTerminal).map((section) => (
|
||||
<LazyDisclosure
|
||||
key={section}
|
||||
title={SECTION_TITLE[section]}
|
||||
startOpen={startsOpen(section)}
|
||||
>
|
||||
<SectionBody section={section} row={row} actions={actions} />
|
||||
</LazyDisclosure>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { tone, label } = FRESHNESS[row.freshness];
|
||||
const declareError = actions.actionErrors.get(row.name);
|
||||
return (
|
||||
<div class="ui-agent-detail">
|
||||
<dl class="ui-agent-detail-fields">
|
||||
<HeaderRows row={row} actions={actions} />
|
||||
<OverviewRows row={row} />
|
||||
<SettingsRows agent={row.name} />
|
||||
<AccountsRows row={row} actions={actions} />
|
||||
<dt>status</dt>
|
||||
<dd>
|
||||
<Badge
|
||||
tone={tone}
|
||||
value={
|
||||
<>
|
||||
{label}
|
||||
{row.last_seen_unix !== null ? (
|
||||
<>
|
||||
{" "}
|
||||
(<RelativeTime epochMs={row.last_seen_unix * 1000} />)
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</dd>
|
||||
<dt>message</dt>
|
||||
<dd>{row.snapshot?.status_text ?? "—"}</dd>
|
||||
<dt>wanted</dt>
|
||||
<dd>
|
||||
{/* Full menu (destroy included) — mara: "destroy is already
|
||||
available via wanted state", no separate button needed. */}
|
||||
<WantedMenu
|
||||
row={row}
|
||||
pending={actions.pendingAgents.has(row.name)}
|
||||
onSelectUp={actions.selectUp}
|
||||
onSelectOffline={actions.selectOffline}
|
||||
onSelectPaused={actions.selectPaused}
|
||||
onDestroy={actions.requestDestroy}
|
||||
/>
|
||||
{/* A failed declaration shows here too: on a terminal page's
|
||||
dialog there is no roster card to carry the "failed" badge. */}
|
||||
{declareError ? (
|
||||
<ApiErrorPanel
|
||||
context="the declaration failed"
|
||||
problem={declareError}
|
||||
/>
|
||||
) : null}
|
||||
</dd>
|
||||
<dt>state</dt>
|
||||
<dd>
|
||||
<AgentStateRow key={row.name} agent={row.name} />
|
||||
</dd>
|
||||
<dt>hive</dt>
|
||||
<dd>{row.hive ?? "—"}</dd>
|
||||
<dt>config PR</dt>
|
||||
<dd>
|
||||
{row.config_pr ? (
|
||||
<Badge
|
||||
tone="warning"
|
||||
value={
|
||||
row.config_pr.html_url ? (
|
||||
<a
|
||||
href={row.config_pr.html_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
#{row.config_pr.pr_number}
|
||||
</a>
|
||||
) : (
|
||||
`#${row.config_pr.pr_number}`
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</dd>
|
||||
<dt>inference</dt>
|
||||
<dd>
|
||||
<AgentInference key={row.name} agent={row.name} />
|
||||
</dd>
|
||||
<dt>instructions</dt>
|
||||
<dd>
|
||||
<AgentInstructions key={row.name} agent={row.name} />
|
||||
</dd>
|
||||
<dt>resource caps</dt>
|
||||
<dd>
|
||||
<AgentLimits key={row.name} agent={row.name} />
|
||||
</dd>
|
||||
<dt>permissions</dt>
|
||||
<dd>
|
||||
<AgentPermissions key={row.name} agent={row.name} />
|
||||
</dd>
|
||||
<dt>allowed domains</dt>
|
||||
<dd>
|
||||
<AllowedDomains key={row.name} agent={row.name} />
|
||||
</dd>
|
||||
<dt>accounts</dt>
|
||||
<dd>
|
||||
{row.hive ? (
|
||||
<LinkedAccounts
|
||||
key={`${row.hive}/${row.name}`}
|
||||
hive={row.hive}
|
||||
agent={row.name}
|
||||
version={actions.linkVersion}
|
||||
/>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
<LinkAccountAction row={row} actions={actions} />
|
||||
<div class="ui-agent-detail-actions">
|
||||
<LinkAccountMenu
|
||||
agent={row.name}
|
||||
hasHive={row.hive !== null}
|
||||
onSelect={(kind) => actions.openLink(row, kind)}
|
||||
/>
|
||||
</div>
|
||||
{withTerminal ? (
|
||||
<SectionBody section="terminal" row={row} actions={actions} />
|
||||
<>
|
||||
{/* The agent's live terminal and its input box. */}
|
||||
<AgentTermPreview agentName={row.name} />
|
||||
<SubagentTerms key={row.name} agentName={row.name} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// <AgentDetailPopover> — an agent's details from `/agents`, as in an opened card, in a popover
|
||||
// <AgentDetailPopover> — the agent detail panel from `/agents`, in a popover
|
||||
// under its anchor, for a page that is already showing the agent's terminal
|
||||
// and should not be left, or dimmed, to read or change its details. It carries
|
||||
// the same body (`AgentDetail`) and the same actions (`useAgentActions`) as the
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// <AgentInference> — one agent's inference settings in an opened agent card: the
|
||||
// <AgentInference> — one agent's inference settings on the detail panel: the
|
||||
// model and effort it runs on now, with where each came from, and an editor
|
||||
// for the value the swarm sets.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// <AgentInstructions> — an editor for an agent's AGENTS.md, in a dialog opened
|
||||
// from the agent's opened card.
|
||||
// from the agent's detail panel.
|
||||
//
|
||||
// It loads the copy the agent's harness last published
|
||||
// (`GET /api/agents/{name}/agents-md`, so a stopped agent shows the text it
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// <AgentLimits> — one agent's resource caps in an opened agent card: the CPU and
|
||||
// <AgentLimits> — one agent's resource caps on the detail panel: the CPU and
|
||||
// memory caps the swarm sets for it, and an editor for them.
|
||||
//
|
||||
// Reads `GET /api/agents/{name}/limits`, which answers the stored caps as
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// <AgentPermissions> — one agent's permissions in an opened agent card: what the
|
||||
// <AgentPermissions> — one agent's permissions on the detail panel: what the
|
||||
// swarm grants it, and an editor for that.
|
||||
//
|
||||
// Reads `GET /api/agents/{name}/permissions`, which answers the stored list as
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// The allowed-domains editor of an opened agent card: which entries it
|
||||
// The detail panel's allowed-domains editor: which entries one agent's panel
|
||||
// shows and the bodies it sends. Kept out of the component so `npm test` can
|
||||
// run it.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// An opened agent card's permissions editor: what it starts on and how a granted
|
||||
// The detail panel's permissions editor: what it starts on and how a granted
|
||||
// set reads. Kept out of the component so `npm test` can run it.
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
// The sections an agent card opens into, without a DOM: which there are, in
|
||||
// what order, and which start open. The card's header already shows the
|
||||
// agent's status, message and wanted state, so the sections hold the rest.
|
||||
|
||||
/** One folded block of an opened agent card. */
|
||||
export type DetailSection = "overview" | "settings" | "accounts" | "terminal";
|
||||
|
||||
/** What each section's header says. */
|
||||
export const SECTION_TITLE: Record<DetailSection, string> = {
|
||||
overview: "overview",
|
||||
settings: "settings",
|
||||
accounts: "accounts",
|
||||
terminal: "terminal",
|
||||
};
|
||||
|
||||
/**
|
||||
* The sections a card shows, top to bottom. The terminal is left out where
|
||||
* the page is already beside one (the terminal page's details dialog).
|
||||
*/
|
||||
export function sectionsFor(withTerminal: boolean): DetailSection[] {
|
||||
const sections: DetailSection[] = ["overview", "settings", "accounts"];
|
||||
return withTerminal ? [...sections, "terminal"] : sections;
|
||||
}
|
||||
|
||||
/** Only the overview is open when a card opens; the rest build on demand. */
|
||||
export function startsOpen(section: DetailSection): boolean {
|
||||
return section === "overview";
|
||||
}
|
||||
|
|
@ -42,35 +42,3 @@
|
|||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* The header is what opens the card: it reads as a control without a box. */
|
||||
.ui-agent-card-row[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* The opened body, set off from the header by a rule. */
|
||||
.ui-agent-card-detail {
|
||||
margin-block-start: 0.75em;
|
||||
padding-block-start: 0.75em;
|
||||
border-block-start: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* The same chevron as a folded card's (`ui/disclosure/`): right while shut,
|
||||
down while open, so the header reads as something that unfolds. */
|
||||
.ui-agent-card-chevron {
|
||||
align-self: center;
|
||||
width: 0.55em;
|
||||
height: 0.55em;
|
||||
flex: none;
|
||||
border-right: 2px solid var(--muted);
|
||||
border-bottom: 2px solid var(--muted);
|
||||
transform: rotate(-45deg);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.ui-agent-card-row[aria-expanded="true"] .ui-agent-card-chevron {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ui-agent-card-chevron {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +1,56 @@
|
|||
// <AgentCard> — one card per roster agent (mara: "main view: name,
|
||||
// status, message, wanted" / "message as second line" / "more like
|
||||
// card per agent"). The header is those four things; selecting it opens the
|
||||
// card in place into the agent's details (mara: "one column of expandable
|
||||
// cards"), so there is no second panel to select into. The header already
|
||||
// shows the agent's status, message and wanted state, so the opened body is the
|
||||
// rest: `AgentDetail`'s folded sections (`inCard`).
|
||||
// card per agent") — the FilterableView `view === "cards"` alternative
|
||||
// to the original `Table`, not its replacement (see `AgentsPage`'s own
|
||||
// comment). Clicking a card opens a detail panel repeating this same
|
||||
// info plus what doesn't fit here (hive, config-PR link, matrix
|
||||
// link-account) — mara: "the info from main list should be included in
|
||||
// the agent view" too, not just the leftovers.
|
||||
//
|
||||
// The `stopPropagation` wrappers around the login and `WantedMenu` controls
|
||||
// keep their own clicks (mouse or keyboard-synthesized, they host real
|
||||
// `<button>`s) from also toggling the card. The wanted menu carries the destroy
|
||||
// option here, since no detail row repeats it any more.
|
||||
// Built on the shared `Card` primitive (`ui/card/`) for the clickable/
|
||||
// selectable-container mechanics — the `stopPropagation` wrapper around
|
||||
// `WantedMenu` below keeps its own clicks (mouse or keyboard-synthesized,
|
||||
// `WantedMenu` hosts real `<button>`s) from also firing `Card`'s
|
||||
// `onClick` and opening the detail panel.
|
||||
import { useState } from "preact/hooks";
|
||||
import { Badge } from "@hive/shared/badge.js";
|
||||
import type { ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { Card } from "../../ui/card/Card.js";
|
||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||
import { AgentIcon } from "../AgentIcon.js";
|
||||
import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js";
|
||||
import { AgentDetail } from "../detail/AgentDetail.js";
|
||||
import { FRESHNESS, showsLoginNeeded, type AgentRow } from "../AgentTypes.js";
|
||||
import type { AgentActions } from "./AgentActions.js";
|
||||
import { WantedMenu } from "./WantedMenu.js";
|
||||
import "./AgentCard.css";
|
||||
|
||||
export function AgentCard({
|
||||
row,
|
||||
actions,
|
||||
pending,
|
||||
error,
|
||||
selected,
|
||||
onSelectUp,
|
||||
onSelectOffline,
|
||||
onSelectPaused,
|
||||
onOpenDetail,
|
||||
}: {
|
||||
row: AgentRow;
|
||||
actions: AgentActions;
|
||||
pending: boolean;
|
||||
error: ProblemDetails | undefined;
|
||||
/** This row is the one currently shown in the detail panel — a
|
||||
* highlight only (the panel itself is the source of truth), so a
|
||||
* page-refresh landing on a still-valid selection reads as obviously
|
||||
* "that one" rather than a silent selection nothing marks. */
|
||||
selected: boolean;
|
||||
onSelectUp: (row: AgentRow) => void;
|
||||
onSelectOffline: (row: AgentRow) => void;
|
||||
onSelectPaused: (row: AgentRow) => void;
|
||||
onOpenDetail: (row: AgentRow) => void;
|
||||
}) {
|
||||
const { tone, label } = FRESHNESS[row.freshness];
|
||||
const [loggingIn, setLoggingIn] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const error = actions.actionErrors.get(row.name);
|
||||
const toggle = () => setOpen((was) => !was);
|
||||
return (
|
||||
<Card>
|
||||
<div
|
||||
class="ui-agent-card-row"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={open}
|
||||
onClick={toggle}
|
||||
onKeyDown={(e) => {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Card selected={selected} onClick={() => onOpenDetail(row)}>
|
||||
<div class="ui-agent-card-row">
|
||||
<AgentIcon name={row.name} />
|
||||
<div class="ui-agent-card-body">
|
||||
<div class="ui-agent-card-line1">
|
||||
|
|
@ -103,11 +104,11 @@ export function AgentCard({
|
|||
>
|
||||
<WantedMenu
|
||||
row={row}
|
||||
pending={actions.pendingAgents.has(row.name)}
|
||||
onSelectUp={actions.selectUp}
|
||||
onSelectOffline={actions.selectOffline}
|
||||
onSelectPaused={actions.selectPaused}
|
||||
onDestroy={actions.requestDestroy}
|
||||
pending={pending}
|
||||
showDestroy={false}
|
||||
onSelectUp={onSelectUp}
|
||||
onSelectOffline={onSelectOffline}
|
||||
onSelectPaused={onSelectPaused}
|
||||
/>
|
||||
{error ? (
|
||||
<Badge
|
||||
|
|
@ -122,13 +123,7 @@ export function AgentCard({
|
|||
{row.snapshot?.status_text ?? "—"}
|
||||
</div>
|
||||
</div>
|
||||
<span class="ui-agent-card-chevron" aria-hidden="true" />
|
||||
</div>
|
||||
{open ? (
|
||||
<div class="ui-agent-card-detail">
|
||||
<AgentDetail row={row} actions={actions} withTerminal inCard />
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ export function WantedMenu({
|
|||
onSelectOffline,
|
||||
onSelectPaused,
|
||||
onDestroy,
|
||||
showDestroy = true,
|
||||
portal = false,
|
||||
}: {
|
||||
row: AgentRow;
|
||||
pending: boolean;
|
||||
|
|
@ -31,6 +33,30 @@ export function WantedMenu({
|
|||
onSelectOffline: (row: AgentRow) => void;
|
||||
onSelectPaused: (row: AgentRow) => void;
|
||||
onDestroy?: (row: AgentRow) => void;
|
||||
/**
|
||||
* The card view (see `AgentCard`) passes `false` here — its
|
||||
* quick-access menu keeps to the three everyday states, so "destroy"
|
||||
* can't be reached by an extra click off a state that's already open.
|
||||
* `AgentsPage`'s detail-panel `WantedMenu` keeps the default (`true`)
|
||||
* — mara: "destroy is already available via wanted state", i.e. the
|
||||
* full menu there, not a separate button.
|
||||
*/
|
||||
showDestroy?: boolean;
|
||||
/**
|
||||
* Forwarded to `Dropdown`'s own `portal` (see its file-top comment) —
|
||||
* only the table's "wanted" column needs it, to escape
|
||||
* `.ui-table-scroll`'s clip on the table's last row (the bug `Dropdown`'s
|
||||
* own `portal` prop exists to fix in the first place).
|
||||
* Neither the card list nor the detail panel has a clipping ancestor,
|
||||
* and inside the detail panel specifically `portal` is actively wrong:
|
||||
* a `position: fixed` element appended to `document.body` renders
|
||||
* *behind* an open native `<dialog>` (the dialog is promoted to the
|
||||
* browser's top layer, which composites above ordinary body content
|
||||
* regardless of z-index) — found while screenshotting this exact
|
||||
* dropdown open inside the detail panel, only the portion extending
|
||||
* past the dialog's own edge was visible.
|
||||
*/
|
||||
portal?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -49,7 +75,9 @@ export function WantedMenu({
|
|||
{ value: "up", label: "up" },
|
||||
{ value: "paused", label: "paused" },
|
||||
{ value: "offline", label: "offline" },
|
||||
{ value: "destroy", label: "destroy", danger: true },
|
||||
...(showDestroy
|
||||
? [{ value: "destroy", label: "destroy", danger: true }]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -75,6 +103,7 @@ export function WantedMenu({
|
|||
/>
|
||||
<Dropdown
|
||||
open={open}
|
||||
portal={portal}
|
||||
options={options}
|
||||
activeValue={row.wanted ?? undefined}
|
||||
label={`declare ${row.name}`}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* <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 an opened agent card rather
|
||||
than competing with the card's own fields for vertical space. */
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// <AgentTermPreview> — the MVP mara's own ruling asked for: not a separate
|
||||
// page, a small preview in an opened agent card on `AgentsPage`, with
|
||||
// page, a small preview embedded below `AgentsPage`'s detail panel fields, with
|
||||
// an `AgentInputForm` under an agent's terminal (a subagent's has none: it
|
||||
// takes no input of its own). Consumes swarm-controller's
|
||||
// `GET /api/agents/{name}/term/stream`, or for a subagent
|
||||
|
|
@ -20,9 +20,9 @@
|
|||
// not from `AgentRow` — the roster fetch `AgentsPage` already did has no
|
||||
// notion of turn_state/model/ctx/cost, only `wanted`/`snapshot.running`.
|
||||
// Only the full-tab mount (`fullHeight`) shows the state badges: the small
|
||||
// preview in an opened card stays free of them (mara: "the preview is small
|
||||
// preview in the detail panel stays free of them (mara: "the preview is small
|
||||
// and should not be encroached further with badges on top, but for the full
|
||||
// screen term i want it like that"), and the card's overview lists the
|
||||
// screen term i want it like that"), and the panel's details table lists the
|
||||
// same state in a row of its own (`AgentStateRow`).
|
||||
|
||||
// **Expand-to-fullscreen trigger** — opens/navigates to
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// <SubagentTerms> — an agent's subagents, under its own terminal preview in
|
||||
// an opened agent card on `AgentsPage`. The list is `GET /api/agents/{name}/subagents`:
|
||||
// `AgentsPage`'s detail panel. The list is `GET /api/agents/{name}/subagents`:
|
||||
// the subagents the swarm queue holds terminal rows for, each with the
|
||||
// publish time of its newest row. Newest first, with a ticking age beside
|
||||
// the name; one quiet for `STALE_S` is dimmed. Subagents are spawned on
|
||||
|
|
|
|||
|
|
@ -474,7 +474,7 @@ export function ComponentsPage() {
|
|||
<div>click, or Enter/Space when focused</div>
|
||||
</Card>
|
||||
</Sample>
|
||||
<Sample label="selected (e.g. an opened row)">
|
||||
<Sample label="selected (e.g. the row shown in a detail panel)">
|
||||
<Card selected onClick={() => {}}>
|
||||
<strong>gamma</strong>
|
||||
<div>same card, with selected set</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// <AgentTerminalPage> — the swarm terminal tab's full page, mounted at
|
||||
// `/agents/:name/terminal`. The MVP `AgentTermPreview` (in an opened
|
||||
// agent card on `AgentsPage`) is height-capped; this is that same
|
||||
// `/agents/:name/terminal`. The MVP `AgentTermPreview` (below `AgentsPage`'s
|
||||
// detail panel) is height-capped; this is that same
|
||||
// component's `fullHeight` mount — no 12em cap, floating header badges
|
||||
// shown (there's no card header here to duplicate them).
|
||||
// shown (there's no detail-panel `<dl>` here to duplicate them).
|
||||
//
|
||||
// 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
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue