swarm ui: the agent card has a terminal icon beside the wanted menu
It opens the agent's terminal tab without opening the card.
This commit is contained in:
parent
0912e2ca06
commit
5b66a83112
3 changed files with 38 additions and 2 deletions
|
|
@ -56,8 +56,9 @@ works too.
|
||||||
### The agents page
|
### The agents page
|
||||||
|
|
||||||
`/agents` is the hives card, then one column of cards, one per agent. A card's
|
`/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**
|
header shows its icon, name, freshness, the status message, a **terminal**
|
||||||
menu (up, offline, paused, destroy), and a **login needed** badge with a
|
button that opens the agent's terminal tab, 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
|
**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,
|
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
|
and a card stays open across the page's refresh, but not across a reload. The
|
||||||
|
|
|
||||||
|
|
@ -105,3 +105,24 @@ export function FilterIcon() {
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A prompt in a window (`>_`), the standard "terminal" glyph — the agent
|
||||||
|
// card's shortcut to the agent's terminal tab. Same recipe as `LinkIcon`.
|
||||||
|
export function TerminalIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="1.3em"
|
||||||
|
height="1.3em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<polyline points="4 17 10 11 4 5" />
|
||||||
|
<line x1="12" y1="19" x2="20" y2="19" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,23 @@
|
||||||
// shows the agent's status, message and wanted state, so the opened body is the
|
// shows the agent's status, message and wanted state, so the opened body is the
|
||||||
// rest: `AgentDetail`'s folded sections (`inCard`).
|
// rest: `AgentDetail`'s folded sections (`inCard`).
|
||||||
//
|
//
|
||||||
|
// The terminal badge beside the wanted menu opens the agent's terminal tab, the
|
||||||
|
// most common action, without opening the card.
|
||||||
|
//
|
||||||
// The `stopPropagation` wrappers around the login and `WantedMenu` controls
|
// The `stopPropagation` wrappers around the login and `WantedMenu` controls
|
||||||
// keep their own clicks (mouse or keyboard-synthesized, they host real
|
// keep their own clicks (mouse or keyboard-synthesized, they host real
|
||||||
// `<button>`s) from also toggling the card. The wanted menu carries the destroy
|
// `<button>`s) from also toggling the card. The wanted menu carries the destroy
|
||||||
// option here, since no detail row repeats it any more.
|
// option here, since no detail row repeats it any more.
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { Badge } from "@hive/shared/badge.js";
|
import { Badge } from "@hive/shared/badge.js";
|
||||||
|
import { TerminalIcon } from "@hive/shared/icons.js";
|
||||||
import { Card } from "../../ui/card/Card.js";
|
import { Card } from "../../ui/card/Card.js";
|
||||||
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
|
||||||
|
import { useDynamicTabs } from "../../shell/useDynamicTabs.js";
|
||||||
import { AgentIcon } from "../AgentIcon.js";
|
import { AgentIcon } from "../AgentIcon.js";
|
||||||
import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js";
|
import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js";
|
||||||
import { AgentDetail } from "../detail/AgentDetail.js";
|
import { AgentDetail } from "../detail/AgentDetail.js";
|
||||||
|
import { termPaths } from "../terminal/AgentTermPreview.js";
|
||||||
import { FRESHNESS, showsLoginNeeded, type AgentRow } from "../AgentTypes.js";
|
import { FRESHNESS, showsLoginNeeded, type AgentRow } from "../AgentTypes.js";
|
||||||
import type { AgentActions } from "./AgentActions.js";
|
import type { AgentActions } from "./AgentActions.js";
|
||||||
import { WantedMenu } from "./WantedMenu.js";
|
import { WantedMenu } from "./WantedMenu.js";
|
||||||
|
|
@ -34,6 +40,8 @@ export function AgentCard({
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const error = actions.actionErrors.get(row.name);
|
const error = actions.actionErrors.get(row.name);
|
||||||
const toggle = () => setOpen((was) => !was);
|
const toggle = () => setOpen((was) => !was);
|
||||||
|
const { open: openTab } = useDynamicTabs();
|
||||||
|
const term = termPaths(row.name);
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div
|
<div
|
||||||
|
|
@ -101,6 +109,12 @@ export function AgentCard({
|
||||||
class="ui-agent-card-wanted"
|
class="ui-agent-card-wanted"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
<Badge
|
||||||
|
variant="quiet"
|
||||||
|
value={<TerminalIcon />}
|
||||||
|
title={`open ${row.name}'s terminal in its own tab`}
|
||||||
|
onClick={() => openTab({ href: term.page, label: term.label })}
|
||||||
|
/>
|
||||||
<WantedMenu
|
<WantedMenu
|
||||||
row={row}
|
row={row}
|
||||||
pending={actions.pendingAgents.has(row.name)}
|
pending={actions.pendingAgents.has(row.name)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue