From d3717b5ae1600d78ca32cbea755cb634b4d4f99c Mon Sep 17 00:00:00 2001 From: Wes Date: Thu, 19 Mar 2026 10:31:39 -0700 Subject: [PATCH 1/2] feat(desktop): redesign Agents page as compact table with actions menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace verbose per-agent cards with a clean table layout. Each agent shows as a single row (name, status, model, runtime) with a 3-dot dropdown for all actions. Model picker auto-discovers on render instead of requiring a manual "Discover" click. Remove relay URL and auth info from the view — static config that wasn't actionable. Delete the unused ManagedAgentCard component and formatTimestamp helper. Normalize border radius from rounded-3xl to rounded-xl to match the rest of the app. Co-Authored-By: Claude Opus 4.6 (1M context) --- desktop/src/features/agents/ui/AgentsView.tsx | 34 +- .../features/agents/ui/ManagedAgentCard.tsx | 182 ----------- .../agents/ui/ManagedAgentLogPanel.tsx | 6 +- .../agents/ui/ManagedAgentsSection.tsx | 298 +++++++++++++++--- .../src/features/agents/ui/ModelPicker.tsx | 79 ++--- .../agents/ui/RelayDirectorySection.tsx | 6 +- desktop/src/features/agents/ui/agentUi.ts | 13 - 7 files changed, 291 insertions(+), 327 deletions(-) delete mode 100644 desktop/src/features/agents/ui/ManagedAgentCard.tsx diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx index c18ae07..9fadf64 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -58,31 +58,28 @@ 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 [logAgentPubkey, setLogAgentPubkey] = React.useState( + null, + ); + const logAgent = + managedAgents.find((agent) => agent.pubkey === logAgentPubkey) ?? null; const managedAgentLogQuery = useManagedAgentLogQuery( - selectedAgent?.pubkey ?? null, + logAgent?.pubkey ?? null, ); 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 +113,8 @@ export function AgentsView() { try { await deleteMutation.mutateAsync(pubkey); - if (selectedAgentPubkey === pubkey) { - setSelectedAgentPubkey(null); + if (logAgentPubkey === pubkey) { + setLogAgentPubkey(null); } } catch (error) { setActionErrorMessage( @@ -240,7 +237,6 @@ export function AgentsView() { void handleMintToken(pubkey, name); }} onRefresh={handleRefresh} - onSelect={setSelectedAgentPubkey} onStart={(pubkey) => { void handleStart(pubkey); }} @@ -250,7 +246,7 @@ export function AgentsView() { onToggleStartOnAppLaunch={(pubkey, startOnAppLaunch) => { void handleToggleStartOnAppLaunch(pubkey, startOnAppLaunch); }} - selectedAgentPubkey={selectedAgent?.pubkey ?? null} + onViewLogs={setLogAgentPubkey} /> diff --git a/desktop/src/features/agents/ui/ManagedAgentCard.tsx b/desktop/src/features/agents/ui/ManagedAgentCard.tsx deleted file mode 100644 index 12cf1e3..0000000 --- a/desktop/src/features/agents/ui/ManagedAgentCard.tsx +++ /dev/null @@ -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 ( -
- - -
-
-

- Relay -

-

{agent.relayUrl}

-
-
-

- Auth -

-

- {agent.hasApiToken ? "Bearer token saved" : "Key-only dev mode"} -

-
- -
- -
- - - {agent.status === "running" ? ( - - ) : ( - - )} - - - - - - - - -
- -
-

Started {formatTimestamp(agent.lastStartedAt)}

-

Stopped {formatTimestamp(agent.lastStoppedAt)}

-
- - {agent.lastError ? ( -

- {agent.lastError} -

- ) : null} -
- ); -} diff --git a/desktop/src/features/agents/ui/ManagedAgentLogPanel.tsx b/desktop/src/features/agents/ui/ManagedAgentLogPanel.tsx index 605ac88..edcc1fa 100644 --- a/desktop/src/features/agents/ui/ManagedAgentLogPanel.tsx +++ b/desktop/src/features/agents/ui/ManagedAgentLogPanel.tsx @@ -33,7 +33,7 @@ export function ManagedAgentLogPanel({ {!selectedAgent ? ( -
+

No local agent selected

@@ -42,14 +42,14 @@ export function ManagedAgentLogPanel({

) : isLoading ? ( -
+
) : ( -
+
{selectedAgent.name} {selectedAgent.status} diff --git a/desktop/src/features/agents/ui/ManagedAgentsSection.tsx b/desktop/src/features/agents/ui/ManagedAgentsSection.tsx index b1bffaf..750963a 100644 --- a/desktop/src/features/agents/ui/ManagedAgentsSection.tsx +++ b/desktop/src/features/agents/ui/ManagedAgentsSection.tsx @@ -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 (
@@ -64,22 +83,23 @@ export function ManagedAgentsSection({
{isLoading ? ( -
+
{["first", "second"].map((key) => (
- - - + + + +
))}
) : null} {!isLoading && agents.length === 0 ? ( -
+

No local agents yet

@@ -90,45 +110,43 @@ export function ManagedAgentsSection({
) : null} - {agents.map((agent) => ( - { - 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 ? ( +
+
+ + + + + + + + + + + {agents.map((agent) => ( + + ))} + +
AgentStatusModelRuntime +
+
+
+ ) : null} {error ? (

@@ -150,3 +168,181 @@ export function ManagedAgentsSection({ ); } + +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 ( + + +

{agent.name}

+

+ {truncatePubkey(agent.pubkey)} +

+ + + + {agent.status} + + + + + + {agent.agentCommand} + + + + + ); +} + +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 ( + + + + + event.preventDefault()} + > + {isRunning ? ( + onStop(agent.pubkey)} + > + + Stop + + ) : ( + onStart(agent.pubkey)} + > + + Spawn + + )} + + onAddToChannel(agent)} + > + + Add to channel + + + onMintToken(agent.pubkey, agent.name)} + > + + Mint token + + + navigator.clipboard.writeText(agent.pubkey)} + > + + Copy pubkey + + + onViewLogs(agent.pubkey)}> + + View logs + + + + onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch) + } + > + + {agent.startOnAppLaunch ? "Disable auto-start" : "Enable auto-start"} + + + + + onDelete(agent.pubkey)} + > + + Delete + + + + ); +} diff --git a/desktop/src/features/agents/ui/ModelPicker.tsx b/desktop/src/features/agents/ui/ModelPicker.tsx index a72fdc4..0e69a10 100644 --- a/desktop/src/features/agents/ui/ModelPicker.tsx +++ b/desktop/src/features/agents/ui/ModelPicker.tsx @@ -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 ( -
-

- Model -

- -
- ); - } - if (loading) { return ( -
-

- Model -

-
- - Discovering… -
-
+ + + ); } if (error) { return ( -
-

- Model -

-

{error}

- -
+ retry + + ); } if (!modelsData?.supportsSwitching) { return ( -
-

- Model -

-

Not configurable

-
+ {displayLabel} ); } return ( -
-

- Model -

+
+ ); } diff --git a/desktop/src/features/agents/ui/RelayDirectorySection.tsx b/desktop/src/features/agents/ui/RelayDirectorySection.tsx index 6f9eb22..a2dfc7a 100644 --- a/desktop/src/features/agents/ui/RelayDirectorySection.tsx +++ b/desktop/src/features/agents/ui/RelayDirectorySection.tsx @@ -37,7 +37,7 @@ export function RelayDirectorySection({
{isLoading ? ( -
+
{["directory-1", "directory-2", "directory-3"].map((key) => (
+

No relay-visible agents yet

@@ -71,7 +71,7 @@ export function RelayDirectorySection({ ) : null} {!isLoading && relayAgents.length > 0 ? ( -
+
Date: Thu, 19 Mar 2026 10:58:02 -0700 Subject: [PATCH 2/2] Fix managed agent log selection after create --- desktop/src/features/agents/hooks.ts | 13 +++++++++++++ desktop/src/features/agents/ui/AgentsView.tsx | 5 ++--- desktop/tests/e2e/smoke.spec.ts | 3 +++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/desktop/src/features/agents/hooks.ts b/desktop/src/features/agents/hooks.ts index c5c7d73..cee4599 100644 --- a/desktop/src/features/agents/hooks.ts +++ b/desktop/src/features/agents/hooks.ts @@ -125,6 +125,19 @@ export function useCreateManagedAgentMutation() { return useMutation({ mutationFn: (input: CreateManagedAgentInput) => createManagedAgent(input), + onSuccess: (created) => { + queryClient.setQueryData( + 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 }); diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx index 9fadf64..a180491 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -63,9 +63,7 @@ export function AgentsView() { ); const logAgent = managedAgents.find((agent) => agent.pubkey === logAgentPubkey) ?? null; - const managedAgentLogQuery = useManagedAgentLogQuery( - logAgent?.pubkey ?? null, - ); + const managedAgentLogQuery = useManagedAgentLogQuery(logAgentPubkey); const managedPubkeys = React.useMemo( () => new Set(managedAgents.map((agent) => agent.pubkey)), [managedAgents], @@ -276,6 +274,7 @@ export function AgentsView() { { + setLogAgentPubkey(result.agent.pubkey); setCreatedAgent(result); }} onOpenChange={setIsCreateOpen} diff --git a/desktop/tests/e2e/smoke.spec.ts b/desktop/tests/e2e/smoke.spec.ts index b7c3dff..3c6e05a 100644 --- a/desktop/tests/e2e/smoke.spec.ts +++ b/desktop/tests/e2e/smoke.spec.ts @@ -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", );