swarm-ui: the agents page is one column of cards that open in place
The roster loses its second column and its table view. Selecting a card opens its details under it as four folded sections: overview, settings, accounts and the terminal. The status, message and wanted rows, which the card header already shows, are gone from the details. The terminal page's details dialog keeps the flat list.
This commit is contained in:
parent
04fcb4ca5c
commit
2eaa02e892
25 changed files with 555 additions and 541 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, 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` | 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/<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,9 +53,31 @@ 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 agent's detail panel on `/agents` shows a small live preview of its
|
||||
An opened agent card on `/agents` shows, in its **terminal** section, 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
|
||||
|
|
@ -113,7 +135,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
|
||||
panel on `/agents`, without the terminal preview. The popover doesn't dim the
|
||||
opened card 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
|
||||
|
|
@ -536,7 +558,7 @@ text stays.
|
|||
|
||||
### Inference settings
|
||||
|
||||
The agent's detail panel shows its model and effort under **inference**, each
|
||||
An opened agent card shows, in its **settings** section, 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.
|
||||
|
|
@ -544,13 +566,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 panel reads `GET /api/agents/{agent}/inference`; before the agent's first
|
||||
The card 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 agent's detail panel has an **edit** button. It
|
||||
The **instructions** row of an opened agent card's **settings** 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.
|
||||
|
|
@ -566,7 +588,7 @@ dialog keeps an unsaved edit for the next time you open it.
|
|||
|
||||
### Resource caps
|
||||
|
||||
The agent's detail panel shows its CPU quota and memory max under **resource
|
||||
An opened agent card shows, under **settings**, 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
|
||||
|
|
@ -601,14 +623,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 panel reads
|
||||
`GET /api/agents/{agent}/limits`; when that read fails, the panel shows 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
|
||||
reason and a **retry** button. [hivectl](../tools/hivectl.md#per-agent-resource-limits)
|
||||
sets the hive's own limits.
|
||||
|
||||
### Permissions
|
||||
|
||||
The agent's detail panel shows what the swarm grants it under **permissions**:
|
||||
An opened agent card shows, under **settings**, 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
|
||||
|
|
@ -625,14 +647,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 panel reads
|
||||
`GET /api/agents/{agent}/permissions`; when that read fails, the panel shows
|
||||
restarts the agent after its current turn. The card reads
|
||||
`GET /api/agents/{agent}/permissions`; when that read fails, the card shows
|
||||
the reason and a **retry** button. `swarmctl agent permissions set` sets the
|
||||
same grant from a shell.
|
||||
|
||||
### Allowed domains
|
||||
|
||||
The agent's detail panel lists the destinations it may reach through its
|
||||
An opened agent card lists, under **settings**, 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
|
||||
|
|
@ -697,7 +719,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 detail panel's editors. Leave the
|
||||
the same fields as the opened card's settings 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
|
||||
|
|
@ -737,13 +759,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.
|
||||
|
||||
The agent's detail panel lists its linked accounts under **accounts**, one row
|
||||
An opened agent card lists its linked accounts in its **accounts** section, 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 panel
|
||||
account of that agent as names and hosts and never a credential. When the card
|
||||
fails to read the accounts, the inference settings, the resource caps or the
|
||||
permissions, it
|
||||
shows the reason as text. A list the panel already loaded stays on screen
|
||||
shows the reason as text. A list the card already loaded stays on screen
|
||||
beneath the error.
|
||||
|
||||
| kind | one row per | name | host |
|
||||
|
|
@ -753,11 +775,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 panel shows it as **forge**,
|
||||
The route names the second kind `forgejo`; the card 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 panel requests the list again when
|
||||
it carries an **own account** badge. The card requests the list again when
|
||||
a link dialog closes.
|
||||
|
||||
- **link a matrix account** — `PUT /api/hives/{hive}/agents/{agent}/matrix-accounts/{account}`,
|
||||
|
|
@ -805,7 +827,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 panel requests the list
|
||||
swarm mints that account and would re-mint it. The card 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,3 +17,11 @@
|
|||
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,8 +1,11 @@
|
|||
// <AgentDetail> — one agent's details: its status, wanted state, hive, config
|
||||
// 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).
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
|
|
@ -20,140 +23,262 @@ 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";
|
||||
|
||||
export function AgentDetail({
|
||||
/** The rows the card header already shows: status, message and wanted. */
|
||||
function HeaderRows({
|
||||
row,
|
||||
actions,
|
||||
withTerminal = false,
|
||||
}: {
|
||||
row: AgentRow;
|
||||
actions: AgentActions;
|
||||
withTerminal?: boolean;
|
||||
}) {
|
||||
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">
|
||||
<dt>status</dt>
|
||||
<dd>
|
||||
<>
|
||||
<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={tone}
|
||||
tone="warning"
|
||||
value={
|
||||
<>
|
||||
{label}
|
||||
{row.last_seen_unix !== null ? (
|
||||
<>
|
||||
{" "}
|
||||
(<RelativeTime epochMs={row.last_seen_unix * 1000} />)
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
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>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}
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</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}
|
||||
/>
|
||||
{/* 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>
|
||||
<div class="ui-agent-detail-actions">
|
||||
<LinkAccountMenu
|
||||
agent={row.name}
|
||||
hasHive={row.hive !== null}
|
||||
onSelect={(kind) => actions.openLink(row, kind)}
|
||||
/>
|
||||
</div>
|
||||
{withTerminal ? (
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
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} />
|
||||
</dl>
|
||||
<LinkAccountAction row={row} actions={actions} />
|
||||
{withTerminal ? (
|
||||
<SectionBody section="terminal" row={row} actions={actions} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// <AgentDetailPopover> — the agent detail panel from `/agents`, in a popover
|
||||
// <AgentDetailPopover> — an agent's details from `/agents`, as in an opened card, 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 on the detail panel: the
|
||||
// <AgentInference> — one agent's inference settings in an opened agent card: 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 detail panel.
|
||||
// from the agent's opened card.
|
||||
//
|
||||
// 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 on the detail panel: the CPU and
|
||||
// <AgentLimits> — one agent's resource caps in an opened agent card: 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 on the detail panel: what the
|
||||
// <AgentPermissions> — one agent's permissions in an opened agent card: 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 detail panel's allowed-domains editor: which entries one agent's panel
|
||||
// The allowed-domains editor of an opened agent card: which entries it
|
||||
// shows and the bodies it sends. Kept out of the component so `npm test` can
|
||||
// run it.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// The detail panel's permissions editor: what it starts on and how a granted
|
||||
// An opened agent card's permissions editor: what it starts on and how a granted
|
||||
// set reads. Kept out of the component so `npm test` can run it.
|
||||
|
||||
/**
|
||||
|
|
|
|||
28
frontend/packages/swarm-ui/src/agent/detail/sections.ts
Normal file
28
frontend/packages/swarm-ui/src/agent/detail/sections.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// 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,3 +42,14 @@
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +1,55 @@
|
|||
// <AgentCard> — one card per roster agent (mara: "main view: name,
|
||||
// status, message, wanted" / "message as second line" / "more like
|
||||
// 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.
|
||||
// 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`).
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
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,
|
||||
pending,
|
||||
error,
|
||||
selected,
|
||||
onSelectUp,
|
||||
onSelectOffline,
|
||||
onSelectPaused,
|
||||
onOpenDetail,
|
||||
actions,
|
||||
}: {
|
||||
row: AgentRow;
|
||||
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;
|
||||
actions: AgentActions;
|
||||
}) {
|
||||
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 selected={selected} onClick={() => onOpenDetail(row)}>
|
||||
<div class="ui-agent-card-row">
|
||||
<Card selected={open}>
|
||||
<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();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AgentIcon name={row.name} />
|
||||
<div class="ui-agent-card-body">
|
||||
<div class="ui-agent-card-line1">
|
||||
|
|
@ -104,11 +103,11 @@ export function AgentCard({
|
|||
>
|
||||
<WantedMenu
|
||||
row={row}
|
||||
pending={pending}
|
||||
showDestroy={false}
|
||||
onSelectUp={onSelectUp}
|
||||
onSelectOffline={onSelectOffline}
|
||||
onSelectPaused={onSelectPaused}
|
||||
pending={actions.pendingAgents.has(row.name)}
|
||||
onSelectUp={actions.selectUp}
|
||||
onSelectOffline={actions.selectOffline}
|
||||
onSelectPaused={actions.selectPaused}
|
||||
onDestroy={actions.requestDestroy}
|
||||
/>
|
||||
{error ? (
|
||||
<Badge
|
||||
|
|
@ -124,6 +123,11 @@ export function AgentCard({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{open ? (
|
||||
<div class="ui-agent-card-detail">
|
||||
<AgentDetail row={row} actions={actions} withTerminal inCard />
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ export function WantedMenu({
|
|||
* 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:
|
||||
* Neither the card list nor the details dialog has a clipping ancestor,
|
||||
* and inside the details dialog 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
|
||||
* dropdown open inside the details dialog, only the portion extending
|
||||
* past the dialog's own edge was visible.
|
||||
*/
|
||||
portal?: boolean;
|
||||
|
|
|
|||
|
|
@ -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 the detail panel rather
|
||||
than competing with the panel's own fields for vertical space. */
|
||||
reads as "peek at what's happening" inside an opened agent card rather
|
||||
than competing with the card'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 embedded below `AgentsPage`'s detail panel fields, with
|
||||
// page, a small preview in an opened agent card on `AgentsPage`, 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 the detail panel stays free of them (mara: "the preview is small
|
||||
// preview in an opened card 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 panel's details table lists the
|
||||
// screen term i want it like that"), and the card's overview 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
|
||||
// `AgentsPage`'s detail panel. The list is `GET /api/agents/{name}/subagents`:
|
||||
// an opened agent card on `AgentsPage`. 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. the row shown in a detail panel)">
|
||||
<Sample label="selected (e.g. an opened row)">
|
||||
<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` (below `AgentsPage`'s
|
||||
// detail panel) is height-capped; this is that same
|
||||
// `/agents/:name/terminal`. The MVP `AgentTermPreview` (in an opened
|
||||
// agent card on `AgentsPage`) is height-capped; 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).
|
||||
// shown (there's no card header 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
|
||||
|
|
|
|||
|
|
@ -1,43 +1,4 @@
|
|||
/* Detail panel's "select an agent" empty state. `FilterableView`
|
||||
(`ui/filterable-view/`) has its own equivalent for its two empty
|
||||
cases — colocated there instead of shared with this one, since the
|
||||
two components have no other reason to depend on each other's CSS. */
|
||||
.ui-agents-empty {
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
padding: 1.25em 0.75em;
|
||||
}
|
||||
/* Two-way view switcher in the Panel's title-row actions — same visual
|
||||
weight as `RefreshIntervalPicker` next to it (quiet until touched),
|
||||
but a segmented pair rather than a select since there's no listbox
|
||||
worth opening for a two-option choice. */
|
||||
.ui-agents-view-toggle {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.4em;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ui-agents-view-toggle button {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--muted);
|
||||
padding: 0.45em 0.75em;
|
||||
font: inherit;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-agents-view-toggle button + button {
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.ui-agents-view-toggle button.active {
|
||||
background: var(--bg-elev);
|
||||
color: var(--fg);
|
||||
}
|
||||
.ui-agents-view-toggle button:not(.active):hover {
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
/* The folded hives card over the agents and details panels. */
|
||||
/* The folded hives card over the agents. */
|
||||
.ui-agents-hives {
|
||||
margin-block-end: 1em;
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue