diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx index 81466f6bb9..a445a3af10 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -40,7 +40,6 @@ import { AddAgentToChannelDialog } from "./AddAgentToChannelDialog"; import { AddTeamToChannelDialog } from "./AddTeamToChannelDialog"; import { BatchImportDialog } from "./BatchImportDialog"; import { CreateAgentDialog } from "./CreateAgentDialog"; -import { ManagedAgentLogPanel } from "./ManagedAgentLogPanel"; import { ManagedAgentsSection } from "./ManagedAgentsSection"; import { PersonaDialog } from "./PersonaDialog"; import { PersonaDeleteDialog } from "./PersonaDeleteDialog"; @@ -132,8 +131,6 @@ export function AgentsView() { 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)), @@ -574,6 +571,13 @@ export function AgentsView() { } isActionPending={isActionPending} isLoading={managedAgentsQuery.isLoading} + logContent={managedAgentLogQuery.data?.content ?? null} + logError={ + managedAgentLogQuery.error instanceof Error + ? managedAgentLogQuery.error + : null + } + logLoading={managedAgentLogQuery.isLoading} personaLabelsById={personaLabelsById} presenceLookup={managedPresenceQuery.data ?? {}} onAddToChannel={(agent) => { @@ -590,6 +594,7 @@ export function AgentsView() { onMintToken={(pubkey, name) => { void handleMintToken(pubkey, name); }} + onSelectLogAgent={setLogAgentPubkey} onStart={(pubkey) => { void handleStart(pubkey); }} @@ -599,7 +604,7 @@ export function AgentsView() { onToggleStartOnAppLaunch={(pubkey, startOnAppLaunch) => { void handleToggleStartOnAppLaunch(pubkey, startOnAppLaunch); }} - onViewLogs={setLogAgentPubkey} + selectedLogAgentPubkey={logAgentPubkey} /> - - diff --git a/desktop/src/features/agents/ui/ManagedAgentLogPanel.tsx b/desktop/src/features/agents/ui/ManagedAgentLogPanel.tsx index edcc1faba5..c1d361528f 100644 --- a/desktop/src/features/agents/ui/ManagedAgentLogPanel.tsx +++ b/desktop/src/features/agents/ui/ManagedAgentLogPanel.tsx @@ -1,6 +1,7 @@ import { CircleAlert } from "lucide-react"; import type { ManagedAgent } from "@/shared/api/types"; +import { cn } from "@/shared/lib/cn"; import { Skeleton } from "@/shared/ui/skeleton"; import { CopyButton } from "./CopyButton"; import { describeLogFile } from "./agentUi"; @@ -10,14 +11,29 @@ export function ManagedAgentLogPanel({ isLoading, logContent, selectedAgent, + variant = "section", }: { error: Error | null; isLoading: boolean; logContent: string | null; selectedAgent: ManagedAgent | null; + variant?: "inline" | "section"; }) { + const isInline = variant === "inline"; + + if (!selectedAgent && isInline) { + return null; + } + return ( -
+

Harness log

@@ -55,7 +71,10 @@ export function ManagedAgentLogPanel({ {selectedAgent.status}
             {logContent?.trim() ? logContent : "No log output yet."}
diff --git a/desktop/src/features/agents/ui/ManagedAgentRow.tsx b/desktop/src/features/agents/ui/ManagedAgentRow.tsx
new file mode 100644
index 0000000000..372414a45a
--- /dev/null
+++ b/desktop/src/features/agents/ui/ManagedAgentRow.tsx
@@ -0,0 +1,438 @@
+import {
+  ChevronDown,
+  ChevronRight,
+  Clipboard,
+  Ellipsis,
+  FileText,
+  KeyRound,
+  Play,
+  Power,
+  Square,
+  Trash2,
+  UserPlus,
+} from "lucide-react";
+
+import { PresenceDot } from "@/features/presence/ui/PresenceBadge";
+import type {
+  ManagedAgent,
+  PresenceLookup,
+  PresenceStatus,
+} from "@/shared/api/types";
+import { cn } from "@/shared/lib/cn";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+  DropdownMenuTrigger,
+} from "@/shared/ui/dropdown-menu";
+import { ManagedAgentLogPanel } from "./ManagedAgentLogPanel";
+import { ModelPicker } from "./ModelPicker";
+import { truncatePubkey } from "./agentUi";
+
+export function ManagedAgentRow({
+  agent,
+  isActionPending,
+  isLogSelected,
+  logContent,
+  logError,
+  logLoading,
+  personaLabelsById,
+  presenceLookup,
+  onAddToChannel,
+  onDelete,
+  onMintToken,
+  onSelectLogAgent,
+  onStart,
+  onStop,
+  onToggleStartOnAppLaunch,
+}: {
+  agent: ManagedAgent;
+  isActionPending: boolean;
+  isLogSelected: boolean;
+  logContent: string | null;
+  logError: Error | null;
+  logLoading: boolean;
+  personaLabelsById: Record;
+  presenceLookup: PresenceLookup;
+  onAddToChannel: (agent: ManagedAgent) => void;
+  onDelete: (pubkey: string) => void;
+  onMintToken: (pubkey: string, name: string) => void;
+  onSelectLogAgent: (pubkey: string | null) => void;
+  onStart: (pubkey: string) => void;
+  onStop: (pubkey: string) => void;
+  onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void;
+}) {
+  const isActive = agent.status === "running" || agent.status === "deployed";
+  const isLocal = agent.backend.type === "local";
+  const runtimeSource =
+    agent.backend.type === "local"
+      ? `ACP ${agent.acpCommand}`
+      : `Provider ${agent.backend.id}`;
+  const personaLabel = agent.personaId
+    ? (personaLabelsById[agent.personaId] ?? null)
+    : null;
+  const presenceStatus = presenceLookup[agent.pubkey.trim().toLowerCase()];
+  const processDetail =
+    agent.pid !== null
+      ? `PID ${agent.pid}`
+      : agent.lastExitCode !== null
+        ? `Exit ${agent.lastExitCode}`
+        : isLocal
+          ? "Ready to launch"
+          : "Managed remotely";
+
+  return (
+    
+
+ {isLocal ? ( + + ) : ( +
+
+ + + +
+
+ )} + +
+ + onSelectLogAgent(pubkey)} + onStart={onStart} + onStop={onStop} + onToggleStartOnAppLaunch={onToggleStartOnAppLaunch} + /> +
+
+ + {isLocal && isLogSelected ? ( +
+ +
+ ) : null} +
+ ); +} + +function AgentSummary({ + agent, + isExpandable, + isLogSelected, + personaLabel, + presenceStatus, +}: { + agent: ManagedAgent; + isExpandable: boolean; + isLogSelected: boolean; + personaLabel: string | null; + presenceStatus: PresenceStatus | undefined; +}) { + return ( +
+
+ {isExpandable ? ( + isLogSelected ? ( + + ) : ( + + ) + ) : ( + + )} + {presenceStatus ? ( + + ) : ( + + )} +
+
+

{agent.name}

+ {personaLabel ? ( + + {personaLabel} + + ) : null} + +
+
+ {truncatePubkey(agent.pubkey)} + {agent.backend.type === "local" ? ( + + {agent.startOnAppLaunch ? "Auto-start" : "Manual start"} + + ) : ( + Remote deployment + )} + {isExpandable ? ( + + {isLogSelected ? "Logs visible inline" : "Click to view logs"} + + ) : null} +
+
+
+
+ ); +} + +function StatusBlock({ + isActive, + processDetail, + status, +}: { + isActive: boolean; + processDetail: string; + status: ManagedAgent["status"]; +}) { + return ( +
+

+ Status +

+ +

{processDetail}

+
+ ); +} + +function RuntimeBlock({ + agent, + runtimeSource, +}: { + agent: ManagedAgent; + runtimeSource: string; +}) { + return ( +
+

+ Runtime +

+

+ {agent.agentCommand} +

+
+ {runtimeSource} + {agent.model ? {agent.model} : null} +
+
+ ); +} + +function AgentActionsMenu({ + agent, + isActionPending, + isActive, + onAddToChannel, + onDelete, + onMintToken, + onOpenLogs, + onStart, + onStop, + onToggleStartOnAppLaunch, +}: { + agent: ManagedAgent; + isActionPending: boolean; + isActive: boolean; + onAddToChannel: (agent: ManagedAgent) => void; + onDelete: (pubkey: string) => void; + onMintToken: (pubkey: string, name: string) => void; + onOpenLogs: (pubkey: string) => void; + onStart: (pubkey: string) => void; + onStop: (pubkey: string) => void; + onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void; +}) { + return ( + + + + + event.preventDefault()} + > + {agent.backend.type === "provider" ? ( + <> + onStart(agent.pubkey)} + > + + {isActive ? "Redeploy" : "Deploy"} + + onStop(agent.pubkey)} + > + + Shutdown + + + ) : isActive ? ( + 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 + + + {agent.backend.type === "local" ? ( + onOpenLogs(agent.pubkey)}> + + View logs + + ) : null} + + {agent.backend.type === "local" ? ( + + onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch) + } + > + + {agent.startOnAppLaunch + ? "Disable auto-start" + : "Enable auto-start"} + + ) : null} + + + + onDelete(agent.pubkey)} + > + + Delete + + + + ); +} + +function AgentOriginBadge({ agent }: { agent: ManagedAgent }) { + return ( + + {agent.backend.type === "local" ? "Local" : "Remote"} + + ); +} + +function AgentStatusBadge({ + isActive, + status, +}: { + isActive: boolean; + status: ManagedAgent["status"]; +}) { + return ( + + {status.replace(/_/g, " ")} + + ); +} diff --git a/desktop/src/features/agents/ui/ManagedAgentsSection.tsx b/desktop/src/features/agents/ui/ManagedAgentsSection.tsx index c4b6f34c24..2c324d0c9b 100644 --- a/desktop/src/features/agents/ui/ManagedAgentsSection.tsx +++ b/desktop/src/features/agents/ui/ManagedAgentsSection.tsx @@ -1,31 +1,10 @@ -import { - Clipboard, - Ellipsis, - FileText, - KeyRound, - Play, - Plus, - Power, - Square, - Trash2, - UserPlus, -} from "lucide-react"; +import { Plus } from "lucide-react"; import type { ManagedAgent, PresenceLookup } from "@/shared/api/types"; -import { cn } from "@/shared/lib/cn"; -import { PresenceDot } from "@/features/presence/ui/PresenceBadge"; import { Button } from "@/shared/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - 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"; +import { ManagedAgentRow } from "./ManagedAgentRow"; export function ManagedAgentsSection({ actionErrorMessage, @@ -34,16 +13,20 @@ export function ManagedAgentsSection({ error, isActionPending, isLoading, + logContent, + logError, + logLoading, personaLabelsById, presenceLookup, onAddToChannel, onCreate, onDelete, onMintToken, + onSelectLogAgent, onStart, onStop, onToggleStartOnAppLaunch, - onViewLogs, + selectedLogAgentPubkey, }: { actionErrorMessage: string | null; actionNoticeMessage: string | null; @@ -51,16 +34,20 @@ export function ManagedAgentsSection({ error: Error | null; isActionPending: boolean; isLoading: boolean; + logContent: string | null; + logError: Error | null; + logLoading: boolean; personaLabelsById: Record; presenceLookup: PresenceLookup; onAddToChannel: (agent: ManagedAgent) => void; onCreate: () => void; onDelete: (pubkey: string) => void; onMintToken: (pubkey: string, name: string) => void; + onSelectLogAgent: (pubkey: string | null) => void; onStart: (pubkey: string) => void; onStop: (pubkey: string) => void; onToggleStartOnAppLaunch: (pubkey: string, startOnAppLaunch: boolean) => void; - onViewLogs: (pubkey: string) => void; + selectedLogAgentPubkey: string | null; }) { return (
@@ -78,9 +65,9 @@ export function ManagedAgentsSection({ @@ -118,41 +105,31 @@ export function ManagedAgentsSection({ ) : null} {!isLoading && agents.length > 0 ? ( -
-
- - - - - - - - - - - {agents.map((agent) => ( - - ))} - -
AgentStatusModelRuntime -
-
+
+ {agents.map((agent) => ( + + ))}
) : null} @@ -176,238 +153,3 @@ export function ManagedAgentsSection({
); } - -function ManagedAgentRow({ - agent, - isActionPending, - personaLabelsById, - presenceLookup, - onAddToChannel, - onDelete, - onMintToken, - onStart, - onStop, - onToggleStartOnAppLaunch, - onViewLogs, -}: { - agent: ManagedAgent; - isActionPending: boolean; - personaLabelsById: Record; - presenceLookup: PresenceLookup; - 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; -}) { - const isActive = agent.status === "running" || agent.status === "deployed"; - const personaLabel = agent.personaId - ? (personaLabelsById[agent.personaId] ?? null) - : null; - const presenceStatus = presenceLookup[agent.pubkey.trim().toLowerCase()]; - - return ( - { - if (agent.backend.type === "local") { - onViewLogs(agent.pubkey); - } - }} - > - -
- {presenceStatus ? ( - - ) : null} -

{agent.name}

-
- {personaLabel ? ( -

- - {personaLabel} - -

- ) : null} -

- {truncatePubkey(agent.pubkey)} -

- - - - {agent.status} - - - e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > - - - {agent.agentCommand} - e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - > - - - - ); -} - -function AgentActionsMenu({ - agent, - isActionPending, - isActive, - onAddToChannel, - onDelete, - onMintToken, - onStart, - onStop, - onToggleStartOnAppLaunch, - onViewLogs, -}: { - agent: ManagedAgent; - isActionPending: boolean; - isActive: 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()} - > - {agent.backend.type === "provider" ? ( - <> - onStart(agent.pubkey)} - > - - {isActive ? "Redeploy" : "Deploy"} - - onStop(agent.pubkey)} - > - - Shutdown - - - ) : isActive ? ( - 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 - - - {agent.backend.type === "local" ? ( - onViewLogs(agent.pubkey)}> - - View logs - - ) : null} - - {agent.backend.type === "local" ? ( - - onToggleStartOnAppLaunch(agent.pubkey, !agent.startOnAppLaunch) - } - > - - {agent.startOnAppLaunch - ? "Disable auto-start" - : "Enable auto-start"} - - ) : null} - - - - onDelete(agent.pubkey)} - > - - Delete - - - - ); -} diff --git a/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx b/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx index 5f9a461809..1bd88d66d6 100644 --- a/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx +++ b/desktop/src/features/workflows/ui/WorkflowDetailPanel.tsx @@ -1,4 +1,4 @@ -import { Pencil, Play, X } from "lucide-react"; +import { ChevronDown, ChevronRight, Pencil, Play, X } from "lucide-react"; import * as React from "react"; import { @@ -34,9 +34,6 @@ export function WorkflowDetailPanel({ const workflow = workflowQuery.data; const runs = runsQuery.data ?? []; - const selectedRun = selectedRunId - ? (runs.find((r) => r.id === selectedRunId) ?? null) - : null; const approvalsQuery = useRunApprovalsQuery(workflowId, selectedRunId); const workflowDescription = workflow ? getWorkflowDescription(workflow.definition) @@ -136,45 +133,101 @@ export function WorkflowDetailPanel({

No runs yet.

) : (
- {runs.map((run) => ( - + + {isSelected ? ( +
+
+ Execution Trace + {approvalsQuery.isFetching ? ( + + Refreshing approvals... + + ) : null} +
+ {approvalsQuery.error instanceof Error ? ( +

+ {approvalsQuery.error.message} +

+ ) : null} + +
+ ) : null}
-
- {new Date(run.createdAt * 1000).toLocaleString()} -
- - ))} + ); + })}
)} - - {selectedRun ? ( -
-

- Execution Trace -

- -
- ) : null} ) : workflowQuery.isError ? (
@@ -190,6 +243,20 @@ export function WorkflowDetailPanel({ ); } +function formatRunDuration( + startedAt: number | null, + completedAt: number | null, +) { + if (startedAt === null || completedAt === null) return null; + const seconds = completedAt - startedAt; + if (seconds < 1) return `${Math.round(seconds * 1000)}ms`; + return `${seconds.toFixed(1)}s`; +} + +function formatStatusLabel(status: string) { + return status.replace(/_/g, " "); +} + function RunStatusBadge({ status }: { status: string }) { const colors: Record = { active: "bg-green-500/15 text-green-500", @@ -205,9 +272,9 @@ function RunStatusBadge({ status }: { status: string }) { return ( - {status} + {formatStatusLabel(status)} ); } diff --git a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx index f2e947402f..5683beed19 100644 --- a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx +++ b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx @@ -233,6 +233,7 @@ export function WorkflowFormBuilder({ {mode === "yaml" ? (