mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
372 lines
11 KiB
TypeScript
372 lines
11 KiB
TypeScript
import {
|
|
Clipboard,
|
|
Ellipsis,
|
|
FileText,
|
|
KeyRound,
|
|
Play,
|
|
Plus,
|
|
Power,
|
|
Square,
|
|
Trash2,
|
|
UserPlus,
|
|
} from "lucide-react";
|
|
|
|
import type { ManagedAgent } from "@/shared/api/types";
|
|
import { cn } from "@/shared/lib/cn";
|
|
import { Button } from "@/shared/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/shared/ui/dropdown-menu";
|
|
import { Skeleton } from "@/shared/ui/skeleton";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
|
import { ModelPicker } from "./ModelPicker";
|
|
import { truncatePubkey } from "./agentUi";
|
|
|
|
export function ManagedAgentsSection({
|
|
actionErrorMessage,
|
|
actionNoticeMessage,
|
|
agents,
|
|
error,
|
|
isActionPending,
|
|
isLoading,
|
|
personaLabelsById,
|
|
onAddToChannel,
|
|
onCreate,
|
|
onDelete,
|
|
onMintToken,
|
|
onStart,
|
|
onStop,
|
|
onToggleStartOnAppLaunch,
|
|
onViewLogs,
|
|
}: {
|
|
actionErrorMessage: string | null;
|
|
actionNoticeMessage: string | null;
|
|
agents: ManagedAgent[];
|
|
error: Error | null;
|
|
isActionPending: boolean;
|
|
isLoading: boolean;
|
|
personaLabelsById: Record<string, string>;
|
|
onAddToChannel: (agent: ManagedAgent) => void;
|
|
onCreate: () => void;
|
|
onDelete: (pubkey: string) => void;
|
|
onMintToken: (pubkey: string, name: string) => void;
|
|
onStart: (pubkey: string) => void;
|
|
onStop: (pubkey: string) => void;
|
|
onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
|
|
onViewLogs: (pubkey: string) => void;
|
|
}) {
|
|
return (
|
|
<section className="space-y-4">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<h3 className="text-sm font-semibold tracking-tight">
|
|
Managed locally
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Saved agent profiles and local ACP process state.
|
|
</p>
|
|
</div>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
aria-label="Create agent"
|
|
onClick={onCreate}
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Create agent</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="overflow-hidden rounded-xl border border-border/70 bg-card/80 shadow-sm">
|
|
{["first", "second"].map((key) => (
|
|
<div
|
|
className="flex items-center gap-4 border-b border-border/60 px-4 py-3 last:border-b-0"
|
|
key={key}
|
|
>
|
|
<Skeleton className="h-4 w-28" />
|
|
<Skeleton className="h-5 w-16 rounded-full" />
|
|
<Skeleton className="h-4 w-24" />
|
|
<Skeleton className="h-4 w-20" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
|
|
{!isLoading && agents.length === 0 ? (
|
|
<div className="rounded-xl border border-dashed border-border/80 bg-card/70 px-6 py-10 text-center">
|
|
<p className="text-sm font-semibold tracking-tight">
|
|
No local agents yet
|
|
</p>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
Create one to generate a keypair, mint a token, and launch the ACP
|
|
harness from the desktop app.
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
|
|
{!isLoading && agents.length > 0 ? (
|
|
<div className="overflow-hidden rounded-xl border border-border/70 bg-card/80 shadow-sm">
|
|
<div className="overflow-x-auto">
|
|
<table
|
|
className="w-full border-collapse text-left text-sm"
|
|
data-testid="managed-agents-table"
|
|
>
|
|
<thead className="bg-muted/35 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
<tr>
|
|
<th className="px-4 py-3">Agent</th>
|
|
<th className="px-4 py-3">Status</th>
|
|
<th className="px-4 py-3">Model</th>
|
|
<th className="px-4 py-3">Runtime</th>
|
|
<th className="w-10 px-3 py-3" />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{agents.map((agent) => (
|
|
<ManagedAgentRow
|
|
agent={agent}
|
|
isActionPending={isActionPending}
|
|
key={agent.pubkey}
|
|
personaLabelsById={personaLabelsById}
|
|
onAddToChannel={onAddToChannel}
|
|
onDelete={onDelete}
|
|
onMintToken={onMintToken}
|
|
onStart={onStart}
|
|
onStop={onStop}
|
|
onToggleStartOnAppLaunch={onToggleStartOnAppLaunch}
|
|
onViewLogs={onViewLogs}
|
|
/>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{error ? (
|
|
<p className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
{error.message}
|
|
</p>
|
|
) : null}
|
|
|
|
{actionNoticeMessage ? (
|
|
<p className="rounded-2xl border border-primary/20 bg-primary/10 px-4 py-3 text-sm text-primary">
|
|
{actionNoticeMessage}
|
|
</p>
|
|
) : null}
|
|
|
|
{actionErrorMessage ? (
|
|
<p className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
{actionErrorMessage}
|
|
</p>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function ManagedAgentRow({
|
|
agent,
|
|
isActionPending,
|
|
personaLabelsById,
|
|
onAddToChannel,
|
|
onDelete,
|
|
onMintToken,
|
|
onStart,
|
|
onStop,
|
|
onToggleStartOnAppLaunch,
|
|
onViewLogs,
|
|
}: {
|
|
agent: ManagedAgent;
|
|
isActionPending: boolean;
|
|
personaLabelsById: Record<string, string>;
|
|
onAddToChannel: (agent: ManagedAgent) => void;
|
|
onDelete: (pubkey: string) => void;
|
|
onMintToken: (pubkey: string, name: string) => void;
|
|
onStart: (pubkey: string) => void;
|
|
onStop: (pubkey: string) => void;
|
|
onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
|
|
onViewLogs: (pubkey: string) => void;
|
|
}) {
|
|
const isRunning = agent.status === "running";
|
|
const personaLabel = agent.personaId
|
|
? (personaLabelsById[agent.personaId] ?? null)
|
|
: null;
|
|
|
|
return (
|
|
<tr
|
|
className="cursor-pointer border-b border-border/60 last:border-b-0 hover:bg-muted/30"
|
|
data-testid={`managed-agent-${agent.pubkey}`}
|
|
onClick={() => onViewLogs(agent.pubkey)}
|
|
>
|
|
<td className="px-4 py-3">
|
|
<p className="truncate font-medium text-foreground">{agent.name}</p>
|
|
{personaLabel ? (
|
|
<p className="mt-1">
|
|
<span className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
{personaLabel}
|
|
</span>
|
|
</p>
|
|
) : null}
|
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
{truncatePubkey(agent.pubkey)}
|
|
</p>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span
|
|
className={cn(
|
|
"inline-flex rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.18em]",
|
|
isRunning
|
|
? "bg-primary text-primary-foreground"
|
|
: "bg-muted text-muted-foreground",
|
|
)}
|
|
>
|
|
{agent.status}
|
|
</span>
|
|
</td>
|
|
<td
|
|
className="px-4 py-3"
|
|
onClick={(e) => e.stopPropagation()}
|
|
onKeyDown={(e) => e.stopPropagation()}
|
|
>
|
|
<ModelPicker agent={agent} />
|
|
</td>
|
|
<td className="px-4 py-3 text-muted-foreground">{agent.agentCommand}</td>
|
|
<td
|
|
className="px-3 py-3"
|
|
onClick={(e) => e.stopPropagation()}
|
|
onKeyDown={(e) => e.stopPropagation()}
|
|
>
|
|
<AgentActionsMenu
|
|
agent={agent}
|
|
isActionPending={isActionPending}
|
|
isRunning={isRunning}
|
|
onAddToChannel={onAddToChannel}
|
|
onDelete={onDelete}
|
|
onMintToken={onMintToken}
|
|
onStart={onStart}
|
|
onStop={onStop}
|
|
onToggleStartOnAppLaunch={onToggleStartOnAppLaunch}
|
|
onViewLogs={onViewLogs}
|
|
/>
|
|
</td>
|
|
</tr>
|
|
);
|
|
}
|
|
|
|
function AgentActionsMenu({
|
|
agent,
|
|
isActionPending,
|
|
isRunning,
|
|
onAddToChannel,
|
|
onDelete,
|
|
onMintToken,
|
|
onStart,
|
|
onStop,
|
|
onToggleStartOnAppLaunch,
|
|
onViewLogs,
|
|
}: {
|
|
agent: ManagedAgent;
|
|
isActionPending: boolean;
|
|
isRunning: boolean;
|
|
onAddToChannel: (agent: ManagedAgent) => void;
|
|
onDelete: (pubkey: string) => void;
|
|
onMintToken: (pubkey: string, name: string) => void;
|
|
onStart: (pubkey: string) => void;
|
|
onStop: (pubkey: string) => void;
|
|
onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
|
|
onViewLogs: (pubkey: string) => void;
|
|
}) {
|
|
return (
|
|
<DropdownMenu modal={false}>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
type="button"
|
|
>
|
|
<Ellipsis className="h-4 w-4" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align="end"
|
|
onCloseAutoFocus={(event) => event.preventDefault()}
|
|
>
|
|
{isRunning ? (
|
|
<DropdownMenuItem
|
|
disabled={isActionPending}
|
|
onClick={() => onStop(agent.pubkey)}
|
|
>
|
|
<Square className="h-4 w-4" />
|
|
Stop
|
|
</DropdownMenuItem>
|
|
) : (
|
|
<DropdownMenuItem
|
|
disabled={isActionPending}
|
|
onClick={() => onStart(agent.pubkey)}
|
|
>
|
|
<Play className="h-4 w-4" />
|
|
Spawn
|
|
</DropdownMenuItem>
|
|
)}
|
|
|
|
<DropdownMenuItem
|
|
disabled={isActionPending}
|
|
onClick={() => onAddToChannel(agent)}
|
|
>
|
|
<UserPlus className="h-4 w-4" />
|
|
Add to channel
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem
|
|
disabled={isActionPending}
|
|
onClick={() => onMintToken(agent.pubkey, agent.name)}
|
|
>
|
|
<KeyRound className="h-4 w-4" />
|
|
Mint token
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem
|
|
onClick={() => navigator.clipboard.writeText(agent.pubkey)}
|
|
>
|
|
<Clipboard className="h-4 w-4" />
|
|
Copy pubkey
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem onClick={() => onViewLogs(agent.pubkey)}>
|
|
<FileText className="h-4 w-4" />
|
|
View logs
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem
|
|
disabled={isActionPending}
|
|
onClick={() =>
|
|
onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch)
|
|
}
|
|
>
|
|
<Power className="h-4 w-4" />
|
|
{agent.startOnAppLaunch ? "Disable auto-start" : "Enable auto-start"}
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuItem
|
|
className="text-destructive focus:text-destructive"
|
|
disabled={isActionPending}
|
|
onClick={() => onDelete(agent.pubkey)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|