mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): clean up agents page UI (#124)
This commit is contained in:
@@ -95,6 +95,7 @@ export function usePersonasQuery() {
|
||||
queryKey: personasQueryKey,
|
||||
queryFn: listPersonas,
|
||||
staleTime: 30_000,
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,6 +126,7 @@ export function useRelayAgentsQuery() {
|
||||
queryKey: relayAgentsQueryKey,
|
||||
queryFn: listRelayAgents,
|
||||
staleTime: 15_000,
|
||||
refetchInterval: 15_000,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -261,12 +261,6 @@ export function AgentsView() {
|
||||
void relayAgentsQuery.refetch();
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
void managedAgentsQuery.refetch();
|
||||
void relayAgentsQuery.refetch();
|
||||
void managedAgentLogQuery.refetch();
|
||||
}
|
||||
|
||||
const isActionPending =
|
||||
startMutation.isPending ||
|
||||
stopMutation.isPending ||
|
||||
@@ -341,9 +335,6 @@ export function AgentsView() {
|
||||
},
|
||||
});
|
||||
}}
|
||||
onRefresh={() => {
|
||||
void personasQuery.refetch();
|
||||
}}
|
||||
personas={personas}
|
||||
/>
|
||||
|
||||
@@ -373,7 +364,6 @@ export function AgentsView() {
|
||||
onMintToken={(pubkey, name) => {
|
||||
void handleMintToken(pubkey, name);
|
||||
}}
|
||||
onRefresh={handleRefresh}
|
||||
onStart={(pubkey) => {
|
||||
void handleStart(pubkey);
|
||||
}}
|
||||
|
||||
@@ -296,7 +296,7 @@ export function CreateAgentOptionToggles({
|
||||
type="button"
|
||||
>
|
||||
<p className="text-sm font-semibold tracking-tight">Mint token</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-foreground/70">
|
||||
{prereqs !== null && !isMintSupported
|
||||
? `Unavailable until ${prereqs.admin.command} is installed.`
|
||||
: "Use `sprout-admin` to create a bearer token for this agent."}
|
||||
@@ -317,7 +317,7 @@ export function CreateAgentOptionToggles({
|
||||
<p className="text-sm font-semibold tracking-tight">
|
||||
Start on app launch
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-foreground/70">
|
||||
Reopen this local ACP harness automatically when the desktop app
|
||||
starts.
|
||||
</p>
|
||||
@@ -339,7 +339,7 @@ export function CreateAgentOptionToggles({
|
||||
<p className="text-sm font-semibold tracking-tight">
|
||||
Spawn after create
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-sm text-foreground/70">
|
||||
{prereqs !== null && !isSpawnSupported
|
||||
? "Requires both the ACP harness and MCP server binaries."
|
||||
: "Start the local ACP harness immediately after the profile is saved."}
|
||||
@@ -392,6 +392,9 @@ export function CreateAgentTokenSection({
|
||||
type="button"
|
||||
>
|
||||
{scope.label}
|
||||
<span className="block text-xs text-foreground/60">
|
||||
{scope.description}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Play,
|
||||
Plus,
|
||||
Power,
|
||||
RefreshCcw,
|
||||
Square,
|
||||
Trash2,
|
||||
UserPlus,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
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";
|
||||
|
||||
@@ -38,7 +38,6 @@ export function ManagedAgentsSection({
|
||||
onCreate,
|
||||
onDelete,
|
||||
onMintToken,
|
||||
onRefresh,
|
||||
onStart,
|
||||
onStop,
|
||||
onToggleStartOnAppLaunch,
|
||||
@@ -55,7 +54,6 @@ export function ManagedAgentsSection({
|
||||
onCreate: () => void;
|
||||
onDelete: (pubkey: string) => void;
|
||||
onMintToken: (pubkey: string, name: string) => void;
|
||||
onRefresh: () => void;
|
||||
onStart: (pubkey: string) => void;
|
||||
onStop: (pubkey: string) => void;
|
||||
onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
|
||||
@@ -72,16 +70,20 @@ export function ManagedAgentsSection({
|
||||
Saved agent profiles and local ACP process state.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={onCreate} type="button">
|
||||
<Plus className="h-4 w-4" />
|
||||
Create agent
|
||||
</Button>
|
||||
<Button onClick={onRefresh} type="button" variant="outline">
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</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 ? (
|
||||
@@ -138,7 +140,6 @@ export function ManagedAgentsSection({
|
||||
onAddToChannel={onAddToChannel}
|
||||
onDelete={onDelete}
|
||||
onMintToken={onMintToken}
|
||||
onModelChanged={onRefresh}
|
||||
onStart={onStart}
|
||||
onStop={onStop}
|
||||
onToggleStartOnAppLaunch={onToggleStartOnAppLaunch}
|
||||
@@ -179,7 +180,6 @@ function ManagedAgentRow({
|
||||
onAddToChannel,
|
||||
onDelete,
|
||||
onMintToken,
|
||||
onModelChanged,
|
||||
onStart,
|
||||
onStop,
|
||||
onToggleStartOnAppLaunch,
|
||||
@@ -191,7 +191,6 @@ function ManagedAgentRow({
|
||||
onAddToChannel: (agent: ManagedAgent) => void;
|
||||
onDelete: (pubkey: string) => void;
|
||||
onMintToken: (pubkey: string, name: string) => void;
|
||||
onModelChanged?: () => void;
|
||||
onStart: (pubkey: string) => void;
|
||||
onStop: (pubkey: string) => void;
|
||||
onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
|
||||
@@ -233,7 +232,7 @@ function ManagedAgentRow({
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<ModelPicker agent={agent} onModelChanged={onModelChanged} />
|
||||
<ModelPicker agent={agent} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">{agent.agentCommand}</td>
|
||||
<td className="px-3 py-3">
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
CopyPlus,
|
||||
Ellipsis,
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCcw,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { CopyPlus, Ellipsis, Info, Pencil, Plus, Trash2 } from "lucide-react";
|
||||
|
||||
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
|
||||
import type { AgentPersona } from "@/shared/api/types";
|
||||
@@ -17,13 +10,18 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
|
||||
function promptPreview(systemPrompt: string) {
|
||||
const [firstLine] = systemPrompt
|
||||
const trimmed = systemPrompt.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const [firstLine] = trimmed
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
return firstLine ?? systemPrompt.trim();
|
||||
return firstLine ?? trimmed;
|
||||
}
|
||||
|
||||
type PersonasSectionProps = {
|
||||
@@ -35,7 +33,6 @@ type PersonasSectionProps = {
|
||||
onDuplicate: (persona: AgentPersona) => void;
|
||||
onEdit: (persona: AgentPersona) => void;
|
||||
onDelete: (persona: AgentPersona) => void;
|
||||
onRefresh: () => void;
|
||||
};
|
||||
|
||||
export function PersonasSection({
|
||||
@@ -47,7 +44,6 @@ export function PersonasSection({
|
||||
onDuplicate,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onRefresh,
|
||||
}: PersonasSectionProps) {
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
@@ -58,114 +54,133 @@ export function PersonasSection({
|
||||
Reusable agent templates for common roles and prompts.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={onCreate} type="button">
|
||||
<Plus className="h-4 w-4" />
|
||||
Create persona
|
||||
</Button>
|
||||
<Button onClick={onRefresh} type="button" variant="outline">
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label="Create persona"
|
||||
onClick={onCreate}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Create persona</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
{["first", "second", "third", "fourth"].map((key) => (
|
||||
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-5">
|
||||
{["first", "second", "third", "fourth", "fifth"].map((key) => (
|
||||
<div
|
||||
className="rounded-xl border border-border/70 bg-card/80 p-3 shadow-sm"
|
||||
className="rounded-xl border border-border/70 bg-card/80 p-2 shadow-sm"
|
||||
key={key}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Skeleton className="h-10 w-10 rounded-lg" />
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-3 w-14 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className="mt-3 h-9 w-full rounded-lg" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isLoading && personas.length > 0 ? (
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
{personas.map((persona) => (
|
||||
<div
|
||||
className="rounded-xl border border-border/70 bg-card/80 p-3 shadow-sm"
|
||||
key={persona.id}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2.5">
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<ProfileAvatar
|
||||
avatarUrl={persona.avatarUrl}
|
||||
className="h-10 w-10 rounded-lg text-xs"
|
||||
label={persona.displayName}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="truncate text-sm font-semibold tracking-tight">
|
||||
{persona.displayName}
|
||||
</p>
|
||||
{persona.isBuiltIn ? (
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Built-in
|
||||
</span>
|
||||
) : null}
|
||||
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-5">
|
||||
{personas.map((persona) => {
|
||||
const preview = promptPreview(persona.systemPrompt);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl border border-border/70 bg-card/80 p-2 shadow-sm"
|
||||
key={persona.id}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2.5">
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<ProfileAvatar
|
||||
avatarUrl={persona.avatarUrl}
|
||||
className="h-8 w-8 rounded-lg text-xs"
|
||||
label={persona.displayName}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="truncate text-sm font-semibold tracking-tight">
|
||||
{persona.displayName}
|
||||
</p>
|
||||
{persona.isBuiltIn ? (
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Built-in
|
||||
</span>
|
||||
) : null}
|
||||
{preview ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
aria-label="View system prompt"
|
||||
className="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
|
||||
type="button"
|
||||
>
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="max-w-xs">
|
||||
<p>{preview}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="flex h-6 w-6 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()}
|
||||
>
|
||||
{!persona.isBuiltIn ? (
|
||||
<DropdownMenuItem
|
||||
disabled={isPending}
|
||||
onClick={() => onEdit(persona)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem
|
||||
disabled={isPending}
|
||||
onClick={() => onDuplicate(persona)}
|
||||
>
|
||||
<CopyPlus className="h-4 w-4" />
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
{!persona.isBuiltIn ? (
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
disabled={isPending}
|
||||
onClick={() => onDelete(persona)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="flex h-6 w-6 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()}
|
||||
>
|
||||
{!persona.isBuiltIn ? (
|
||||
<DropdownMenuItem
|
||||
disabled={isPending}
|
||||
onClick={() => onEdit(persona)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem
|
||||
disabled={isPending}
|
||||
onClick={() => onDuplicate(persona)}
|
||||
>
|
||||
<CopyPlus className="h-4 w-4" />
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
{!persona.isBuiltIn ? (
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
disabled={isPending}
|
||||
onClick={() => onDelete(persona)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 line-clamp-2 text-xs leading-5 text-muted-foreground">
|
||||
{promptPreview(persona.systemPrompt)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -3,19 +3,49 @@ import type { TokenScope } from "@/shared/api/types";
|
||||
export const TOKEN_SCOPE_OPTIONS: Array<{
|
||||
value: TokenScope;
|
||||
label: string;
|
||||
description: string;
|
||||
}> = [
|
||||
{ value: "messages:read", label: "Messages: Read" },
|
||||
{ value: "messages:write", label: "Messages: Write" },
|
||||
{ value: "channels:read", label: "Channels: Read" },
|
||||
{ value: "channels:write", label: "Channels: Write" },
|
||||
{ value: "users:read", label: "Users: Read" },
|
||||
{ value: "files:read", label: "Files: Read" },
|
||||
{ value: "files:write", label: "Files: Write" },
|
||||
{
|
||||
value: "messages:read",
|
||||
label: "Messages: Read",
|
||||
description: "Read messages in joined channels",
|
||||
},
|
||||
{
|
||||
value: "messages:write",
|
||||
label: "Messages: Write",
|
||||
description: "Send messages and replies",
|
||||
},
|
||||
{
|
||||
value: "channels:read",
|
||||
label: "Channels: Read",
|
||||
description: "List and view channel metadata",
|
||||
},
|
||||
{
|
||||
value: "channels:write",
|
||||
label: "Channels: Write",
|
||||
description: "Create and update channels",
|
||||
},
|
||||
{
|
||||
value: "users:read",
|
||||
label: "Users: Read",
|
||||
description: "View user profiles and presence",
|
||||
},
|
||||
{
|
||||
value: "files:read",
|
||||
label: "Files: Read",
|
||||
description: "Download uploaded files",
|
||||
},
|
||||
{
|
||||
value: "files:write",
|
||||
label: "Files: Write",
|
||||
description: "Upload files to channels",
|
||||
},
|
||||
];
|
||||
|
||||
export const MANAGED_AGENT_SCOPE_OPTIONS: Array<{
|
||||
value: TokenScope;
|
||||
label: string;
|
||||
description: string;
|
||||
}> = [...TOKEN_SCOPE_OPTIONS];
|
||||
|
||||
export const DEFAULT_MANAGED_AGENT_SCOPES: TokenScope[] = [
|
||||
|
||||
Reference in New Issue
Block a user