Redesign Agents page: compact table with actions menu (#117)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wes
2026-03-19 11:04:06 -07:00
committed by GitHub
parent d074a408c0
commit 3b18846d30
9 changed files with 307 additions and 328 deletions

View File

@@ -125,6 +125,19 @@ export function useCreateManagedAgentMutation() {
return useMutation({
mutationFn: (input: CreateManagedAgentInput) => createManagedAgent(input),
onSuccess: (created) => {
queryClient.setQueryData<ManagedAgent[]>(
managedAgentsQueryKey,
(current) => {
const next = current ?? [];
return [
created.agent,
...next.filter((agent) => agent.pubkey !== created.agent.pubkey),
];
},
);
},
onSettled: async () => {
await queryClient.invalidateQueries({ queryKey: managedAgentsQueryKey });
await queryClient.invalidateQueries({ queryKey: relayAgentsQueryKey });

View File

@@ -58,31 +58,26 @@ export function AgentsView() {
}),
[managedAgentsQuery.data],
);
const [selectedAgentPubkey, setSelectedAgentPubkey] = React.useState<
string | null
>(null);
const selectedAgent =
managedAgents.find((agent) => agent.pubkey === selectedAgentPubkey) ??
managedAgents[0] ??
null;
const managedAgentLogQuery = useManagedAgentLogQuery(
selectedAgent?.pubkey ?? null,
const [logAgentPubkey, setLogAgentPubkey] = React.useState<string | null>(
null,
);
const logAgent =
managedAgents.find((agent) => agent.pubkey === logAgentPubkey) ?? null;
const managedAgentLogQuery = useManagedAgentLogQuery(logAgentPubkey);
const managedPubkeys = React.useMemo(
() => new Set(managedAgents.map((agent) => agent.pubkey)),
[managedAgents],
);
// Clear log selection if the agent was removed
React.useEffect(() => {
if (
selectedAgentPubkey &&
managedAgents.some((agent) => agent.pubkey === selectedAgentPubkey)
logAgentPubkey &&
!managedAgents.some((agent) => agent.pubkey === logAgentPubkey)
) {
return;
setLogAgentPubkey(null);
}
setSelectedAgentPubkey(managedAgents[0]?.pubkey ?? null);
}, [managedAgents, selectedAgentPubkey]);
}, [managedAgents, logAgentPubkey]);
async function handleStart(pubkey: string) {
setActionNoticeMessage(null);
@@ -116,8 +111,8 @@ export function AgentsView() {
try {
await deleteMutation.mutateAsync(pubkey);
if (selectedAgentPubkey === pubkey) {
setSelectedAgentPubkey(null);
if (logAgentPubkey === pubkey) {
setLogAgentPubkey(null);
}
} catch (error) {
setActionErrorMessage(
@@ -240,7 +235,6 @@ export function AgentsView() {
void handleMintToken(pubkey, name);
}}
onRefresh={handleRefresh}
onSelect={setSelectedAgentPubkey}
onStart={(pubkey) => {
void handleStart(pubkey);
}}
@@ -250,7 +244,7 @@ export function AgentsView() {
onToggleStartOnAppLaunch={(pubkey, startOnAppLaunch) => {
void handleToggleStartOnAppLaunch(pubkey, startOnAppLaunch);
}}
selectedAgentPubkey={selectedAgent?.pubkey ?? null}
onViewLogs={setLogAgentPubkey}
/>
<RelayDirectorySection
@@ -273,13 +267,14 @@ export function AgentsView() {
}
isLoading={managedAgentLogQuery.isLoading}
logContent={managedAgentLogQuery.data?.content ?? null}
selectedAgent={selectedAgent}
selectedAgent={logAgent}
/>
</div>
</div>
<CreateAgentDialog
onCreated={(result) => {
setLogAgentPubkey(result.agent.pubkey);
setCreatedAgent(result);
}}
onOpenChange={setIsCreateOpen}

View File

@@ -1,182 +0,0 @@
import {
KeyRound,
Play,
Square,
TerminalSquare,
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 { CopyButton } from "./CopyButton";
import { ModelPicker } from "./ModelPicker";
import { formatTimestamp, truncatePubkey } from "./agentUi";
export function ManagedAgentCard({
agent,
onToggleStartOnAppLaunch,
onAddToChannel,
isSelected,
onDelete,
onMintToken,
onModelChanged,
onSelect,
onStart,
onStop,
}: {
agent: ManagedAgent;
onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
onAddToChannel: (agent: ManagedAgent) => void;
isSelected: boolean;
onDelete: (pubkey: string) => void;
onMintToken: (pubkey: string, name: string) => void;
onModelChanged?: () => void;
onSelect: (pubkey: string) => void;
onStart: (pubkey: string) => void;
onStop: (pubkey: string) => void;
}) {
const statusBadgeClass =
agent.status === "running"
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground";
return (
<article
className={cn(
"rounded-3xl border p-4 shadow-sm transition-colors",
isSelected
? "border-primary/60 bg-primary/5"
: "border-border/70 bg-card/80 hover:bg-card",
)}
data-testid={`managed-agent-${agent.pubkey}`}
>
<button
className="w-full text-left"
onClick={() => onSelect(agent.pubkey)}
type="button"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2">
<h3 className="truncate text-sm font-semibold tracking-tight">
{agent.name}
</h3>
<span
className={cn(
"rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.18em]",
statusBadgeClass,
)}
>
{agent.status}
</span>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{truncatePubkey(agent.pubkey)}
{agent.pid ? ` · pid ${agent.pid}` : ""}
</p>
</div>
<div className="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
<TerminalSquare className="h-4 w-4" />
<span>{agent.agentCommand}</span>
</div>
</div>
</button>
<div className="mt-4 grid gap-2 sm:grid-cols-2">
<div className="rounded-2xl border border-border/60 bg-background/70 px-3 py-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Relay
</p>
<p className="mt-1 truncate text-sm">{agent.relayUrl}</p>
</div>
<div className="rounded-2xl border border-border/60 bg-background/70 px-3 py-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Auth
</p>
<p className="mt-1 text-sm">
{agent.hasApiToken ? "Bearer token saved" : "Key-only dev mode"}
</p>
</div>
<ModelPicker agent={agent} onModelChanged={onModelChanged} />
</div>
<div className="mt-4 flex flex-wrap gap-2">
<CopyButton label="Copy pubkey" value={agent.pubkey} />
{agent.status === "running" ? (
<Button
onClick={() => onStop(agent.pubkey)}
size="sm"
type="button"
variant="outline"
>
<Square className="h-3.5 w-3.5" />
Stop
</Button>
) : (
<Button onClick={() => onStart(agent.pubkey)} size="sm" type="button">
<Play className="h-3.5 w-3.5" />
Spawn
</Button>
)}
<Button
onClick={() => onAddToChannel(agent)}
size="sm"
type="button"
variant="outline"
>
<UserPlus className="h-3.5 w-3.5" />
Add to channel
</Button>
<Button
onClick={() => onMintToken(agent.pubkey, agent.name)}
size="sm"
type="button"
variant="outline"
>
<KeyRound className="h-3.5 w-3.5" />
Mint token
</Button>
<Button
onClick={() =>
onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch)
}
size="sm"
type="button"
variant="outline"
>
{agent.startOnAppLaunch
? "Launch on open: on"
: "Launch on open: off"}
</Button>
<Button
onClick={() => onDelete(agent.pubkey)}
size="sm"
type="button"
variant="ghost"
>
<Trash2 className="h-3.5 w-3.5" />
Delete
</Button>
</div>
<div className="mt-4 grid gap-2 text-xs text-muted-foreground sm:grid-cols-2">
<p>Started {formatTimestamp(agent.lastStartedAt)}</p>
<p>Stopped {formatTimestamp(agent.lastStoppedAt)}</p>
</div>
{agent.lastError ? (
<p className="mt-3 rounded-2xl border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{agent.lastError}
</p>
) : null}
</article>
);
}

View File

@@ -33,7 +33,7 @@ export function ManagedAgentLogPanel({
</div>
{!selectedAgent ? (
<div className="mt-4 rounded-3xl border border-dashed border-border/80 bg-background/70 px-6 py-10 text-center">
<div className="mt-4 rounded-xl border border-dashed border-border/80 bg-background/70 px-6 py-10 text-center">
<p className="text-sm font-semibold tracking-tight">
No local agent selected
</p>
@@ -42,14 +42,14 @@ export function ManagedAgentLogPanel({
</p>
</div>
) : isLoading ? (
<div className="mt-4 rounded-3xl border border-border/70 bg-background/80 p-4">
<div className="mt-4 rounded-xl border border-border/70 bg-background/80 p-4">
<Skeleton className="h-4 w-48" />
<Skeleton className="mt-3 h-4 w-full" />
<Skeleton className="mt-2 h-4 w-full" />
<Skeleton className="mt-2 h-4 w-3/4" />
</div>
) : (
<div className="mt-4 overflow-hidden rounded-3xl border border-border/70 bg-[#17171d] text-[12px] text-zinc-100">
<div className="mt-4 overflow-hidden rounded-xl border border-border/70 bg-[#17171d] text-[12px] text-zinc-100">
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2 text-[11px] uppercase tracking-[0.18em] text-zinc-400">
<span>{selectedAgent.name}</span>
<span>{selectedAgent.status}</span>

View File

@@ -1,9 +1,30 @@
import { Plus, RefreshCcw } from "lucide-react";
import {
Clipboard,
Ellipsis,
FileText,
KeyRound,
Play,
Plus,
Power,
RefreshCcw,
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 { ManagedAgentCard } from "./ManagedAgentCard";
import { ModelPicker } from "./ModelPicker";
import { truncatePubkey } from "./agentUi";
export function ManagedAgentsSection({
actionErrorMessage,
@@ -12,16 +33,15 @@ export function ManagedAgentsSection({
error,
isActionPending,
isLoading,
selectedAgentPubkey,
onAddToChannel,
onCreate,
onDelete,
onMintToken,
onRefresh,
onSelect,
onStart,
onStop,
onToggleStartOnAppLaunch,
onViewLogs,
}: {
actionErrorMessage: string | null;
actionNoticeMessage: string | null;
@@ -29,16 +49,15 @@ export function ManagedAgentsSection({
error: Error | null;
isActionPending: boolean;
isLoading: boolean;
selectedAgentPubkey: string | null;
onAddToChannel: (agent: ManagedAgent) => void;
onCreate: () => void;
onDelete: (pubkey: string) => void;
onMintToken: (pubkey: string, name: string) => void;
onRefresh: () => void;
onSelect: (pubkey: 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">
@@ -64,22 +83,23 @@ export function ManagedAgentsSection({
</div>
{isLoading ? (
<div className="grid gap-3">
<div className="overflow-hidden rounded-xl border border-border/70 bg-card/80 shadow-sm">
{["first", "second"].map((key) => (
<div
className="rounded-3xl border border-border/70 bg-card/80 p-4"
className="flex items-center gap-4 border-b border-border/60 px-4 py-3 last:border-b-0"
key={key}
>
<Skeleton className="h-5 w-32" />
<Skeleton className="mt-3 h-4 w-48" />
<Skeleton className="mt-4 h-16 w-full" />
<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-3xl border border-dashed border-border/80 bg-card/70 px-6 py-10 text-center">
<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>
@@ -90,45 +110,43 @@ export function ManagedAgentsSection({
</div>
) : null}
{agents.map((agent) => (
<ManagedAgentCard
agent={agent}
isSelected={selectedAgentPubkey === agent.pubkey}
key={agent.pubkey}
onAddToChannel={(managedAgent) => {
if (!isActionPending) {
onAddToChannel(managedAgent);
}
}}
onDelete={(pubkey) => {
if (!isActionPending) {
onDelete(pubkey);
}
}}
onMintToken={(pubkey, name) => {
if (!isActionPending) {
onMintToken(pubkey, name);
}
}}
onModelChanged={onRefresh}
onSelect={onSelect}
onStart={(pubkey) => {
if (!isActionPending) {
onStart(pubkey);
}
}}
onStop={(pubkey) => {
if (!isActionPending) {
onStop(pubkey);
}
}}
onToggleStartOnAppLaunch={(pubkey, startOnAppLaunch) => {
if (!isActionPending) {
onToggleStartOnAppLaunch(pubkey, startOnAppLaunch);
}
}}
/>
))}
{!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}
onAddToChannel={onAddToChannel}
onDelete={onDelete}
onMintToken={onMintToken}
onModelChanged={onRefresh}
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">
@@ -150,3 +168,181 @@ export function ManagedAgentsSection({
</section>
);
}
function ManagedAgentRow({
agent,
isActionPending,
onAddToChannel,
onDelete,
onMintToken,
onModelChanged,
onStart,
onStop,
onToggleStartOnAppLaunch,
onViewLogs,
}: {
agent: ManagedAgent;
isActionPending: boolean;
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;
onViewLogs: (pubkey: string) => void;
}) {
const isRunning = agent.status === "running";
return (
<tr
className="border-b border-border/60 last:border-b-0 hover:bg-muted/30"
data-testid={`managed-agent-${agent.pubkey}`}
>
<td className="px-4 py-3">
<p className="truncate font-medium text-foreground">{agent.name}</p>
<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">
<ModelPicker agent={agent} onModelChanged={onModelChanged} />
</td>
<td className="px-4 py-3 text-muted-foreground">{agent.agentCommand}</td>
<td className="px-3 py-3">
<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>
);
}

View File

@@ -39,12 +39,17 @@ export function ModelPicker({
}
}, [agent.pubkey]);
// Auto-fetch on mount
React.useEffect(() => {
void fetchModels();
}, [fetchModels]);
const currentValue = agent.model ?? modelsData?.agentDefaultModel ?? "";
const displayLabel =
agent.model ??
(modelsData?.agentDefaultModel
? `${modelsData.agentDefaultModel} (default)`
: "Select model…");
: "—");
const handleModelChange = async (modelId: string) => {
setSaving(true);
@@ -64,79 +69,41 @@ export function ModelPicker({
}
};
if (!modelsData && !loading && !error) {
return (
<div className="rounded-2xl border border-border/60 bg-background/70 px-3 py-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Model
</p>
<Button
className="mt-1 h-7 px-2 text-sm"
onClick={fetchModels}
size="sm"
type="button"
variant="outline"
>
Discover models
</Button>
</div>
);
}
if (loading) {
return (
<div className="rounded-2xl border border-border/60 bg-background/70 px-3 py-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Model
</p>
<div className="mt-1 flex items-center gap-1.5 text-sm text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Discovering…
</div>
</div>
<span className="inline-flex items-center gap-1.5 text-sm text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
</span>
);
}
if (error) {
return (
<div className="rounded-2xl border border-destructive/30 bg-destructive/10 px-3 py-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-destructive">
Model
</p>
<p className="mt-1 text-sm text-destructive">{error}</p>
<Button
className="mt-1 h-6 px-2 text-xs"
<span className="inline-flex items-center gap-1.5 text-sm">
<span className="text-destructive">Failed</span>
<button
className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground"
onClick={fetchModels}
size="sm"
type="button"
variant="outline"
>
Retry
</Button>
</div>
retry
</button>
</span>
);
}
if (!modelsData?.supportsSwitching) {
return (
<div className="rounded-2xl border border-border/60 bg-background/70 px-3 py-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Model
</p>
<p className="mt-1 text-sm text-muted-foreground">Not configurable</p>
</div>
<span className="text-sm text-muted-foreground">{displayLabel}</span>
);
}
return (
<div className="rounded-2xl border border-border/60 bg-background/70 px-3 py-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Model
</p>
<span className="inline-flex items-center gap-1.5">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
className="mt-1 h-7 max-w-full justify-start gap-1.5 rounded-full border border-border/50 bg-muted/45 px-2.5 text-xs font-medium text-foreground shadow-none hover:bg-muted/70"
className="h-7 max-w-full justify-start gap-1.5 rounded-full border border-border/50 bg-muted/45 px-2.5 text-xs font-medium text-foreground shadow-none hover:bg-muted/70"
disabled={saving}
size="sm"
type="button"
@@ -164,10 +131,10 @@ export function ModelPicker({
</DropdownMenuContent>
</DropdownMenu>
{needsRestart ? (
<p className="mt-1 text-[10px] text-amber-600 dark:text-amber-400">
Restart agent to apply
</p>
<span className="text-[10px] text-amber-600 dark:text-amber-400">
restart to apply
</span>
) : null}
</div>
</span>
);
}

View File

@@ -37,7 +37,7 @@ export function RelayDirectorySection({
</div>
{isLoading ? (
<div className="overflow-hidden rounded-3xl border border-border/70 bg-card/80 shadow-sm">
<div className="overflow-hidden rounded-xl border border-border/70 bg-card/80 shadow-sm">
<div className="grid gap-0">
{["directory-1", "directory-2", "directory-3"].map((key) => (
<div
@@ -59,7 +59,7 @@ export function RelayDirectorySection({
) : null}
{!isLoading && relayAgents.length === 0 ? (
<div className="rounded-3xl border border-dashed border-border/80 bg-card/70 px-6 py-10 text-center">
<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 relay-visible agents yet
</p>
@@ -71,7 +71,7 @@ export function RelayDirectorySection({
) : null}
{!isLoading && relayAgents.length > 0 ? (
<div className="overflow-hidden rounded-3xl border border-border/70 bg-card/80 shadow-sm">
<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"

View File

@@ -1,16 +1,3 @@
export function formatTimestamp(value: string | null) {
if (!value) {
return "Never";
}
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
}).format(new Date(value));
}
export function truncatePubkey(pubkey: string) {
return `${pubkey.slice(0, 8)}…${pubkey.slice(-6)}`;
}

View File

@@ -90,6 +90,9 @@ test("create agent supports parallelism and system prompt overrides", async ({
).toBeVisible();
await page.getByRole("button", { name: "Done" }).click();
await expect(page.getByTestId("managed-agents-table")).toContainText(
agentName,
);
await expect(page.getByTestId("managed-agent-log-content")).toContainText(
"parallelism=3",
);