diff --git a/desktop/src/features/agents/hooks.ts b/desktop/src/features/agents/hooks.ts index c5c7d73276..cee45998f2 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 c18ae07958..a1804911ef 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -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( + 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} /> { + setLogAgentPubkey(result.agent.pubkey); setCreatedAgent(result); }} onOpenChange={setIsCreateOpen} diff --git a/desktop/src/features/agents/ui/ManagedAgentCard.tsx b/desktop/src/features/agents/ui/ManagedAgentCard.tsx deleted file mode 100644 index 12cf1e36fd..0000000000 --- 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 605ac88351..edcc1faba5 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 b1bffafa38..750963a9eb 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 a72fdc4b1d..0e69a10f1b 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 6f9eb22a68..a2dfc7a05d 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 ? ( -
+