feat(desktop): clean up agents page UI (#124)

This commit is contained in:
Wes
2026-03-19 18:18:30 -07:00
committed by GitHub
parent 94203d730c
commit b68222a859
6 changed files with 174 additions and 135 deletions
+2
View File
@@ -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[] = [