I mean, it's at a good place rn...

This commit is contained in:
Renn F
2026-04-20 15:10:54 +02:00
parent 0023c25d60
commit 8e201901c0
264 changed files with 36484 additions and 748 deletions
+106
View File
@@ -0,0 +1,106 @@
"use client";
import Link from "next/link";
import { useStopAgent } from "@/hooks/use-agents";
import { AgentStatusResponse } from "@/types";
import { AgentDefinition } from "@/lib/agent-definitions";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreHorizontal, Activity, Square } from "lucide-react";
import { toast } from "sonner";
import { AgentStateBadge } from "./agent-state-badge";
import { SpawnAgentDialog } from "./spawn-agent-dialog";
interface AgentCardProps {
agent: AgentDefinition;
agentStatus: AgentStatusResponse | null;
}
export function AgentCard({ agent, agentStatus }: AgentCardProps) {
const stopAgent = useStopAgent();
const state = agentStatus?.state || "stopped";
const isActive = ["running", "ready", "starting", "waiting_long"].includes(state);
const handleStop = async (graceful: boolean) => {
try {
await stopAgent.mutateAsync({ agentId: agent.id, graceful });
const message = graceful ? "stopping gracefully" : "force stopped";
toast.success("Agent " + agent.name + " " + message);
} catch {
toast.error("Failed to stop agent");
}
};
return (
<Card className={isActive ? "border-green-500/50" : ""}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base">{agent.name || "Unknown Agent"}</CardTitle>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{!isActive && (
<SpawnAgentDialog agentId={agent.id} agentName={agent.name} />
)}
{isActive && (
<>
<DropdownMenuItem asChild>
<Link href={"/agents/" + agent.id}>
<Activity className="h-4 w-4 mr-2" />
View Details
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleStop(true)}>
<Square className="h-4 w-4 mr-2" />
Stop Gracefully
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleStop(false)}
className="text-red-600"
>
<Square className="h-4 w-4 mr-2" />
Force Stop
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
<CardDescription className="text-xs">
{agent.role?.replace(/_/g, " ") || "N/A"}
{agent.team && " • " + agent.team.replace(/_/g, " ")}
</CardDescription>
</CardHeader>
<CardContent>
<AgentStateBadge state={state} />
{agentStatus?.task_id && (
<p className="text-xs text-muted-foreground mt-2">
Task: {agentStatus.task_id.slice(0, 8)}...
</p>
)}
{agentStatus?.waiting_for && (
<p className="text-xs text-yellow-600 mt-2 truncate">
Waiting: {agentStatus.waiting_for}
</p>
)}
{agentStatus && agentStatus.error_count > 0 && (
<p className="text-xs text-red-500 mt-2">
Errors: {agentStatus.error_count}
</p>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,53 @@
import { AgentStatusResponse } from "@/types";
import { AgentDefinition } from "@/lib/agent-definitions";
import { Card, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { AgentCard } from "./agent-card";
interface AgentGridProps {
title: string;
agents: AgentDefinition[];
agentStatuses: Record<string, AgentStatusResponse>;
isLoading: boolean;
columns?: number;
}
export function AgentGrid({
title,
agents,
agentStatuses,
isLoading,
columns = 4
}: AgentGridProps) {
const gridCols = {
3: "md:grid-cols-3",
4: "md:grid-cols-3 lg:grid-cols-4",
5: "md:grid-cols-3 lg:grid-cols-5",
}[columns] || "md:grid-cols-3 lg:grid-cols-4";
return (
<div>
<h2 className="text-xl font-semibold mb-4">{title}</h2>
<div className={"grid gap-4 " + gridCols}>
{isLoading ? (
Array.from({ length: agents.length || 3 }).map((_, i) => (
<Card key={i}>
<CardHeader>
<Skeleton className="h-5 w-32" />
<Skeleton className="h-3 w-24" />
</CardHeader>
</Card>
))
) : (
agents.map((agent) => (
<AgentCard
key={agent.id}
agent={agent}
agentStatus={agentStatuses[agent.id] || null}
/>
))
)}
</div>
</div>
);
}
@@ -0,0 +1,284 @@
"use client";
import { useMemo } from "react";
import { useAgentDefinitions } from "@/hooks/use-agents";
import { Team, AgentRole } from "@/types";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { User, Users } from "lucide-react";
import { resolveToSlug } from "@/lib/agent-utils";
interface AgentSelectorProps {
value: string | null;
onChange: (value: string | null) => void;
placeholder?: string;
filterByTeam?: Team;
filterByRoles?: AgentRole[];
disabled?: boolean;
allowClear?: boolean;
}
// Role display names
const ROLE_LABELS: Record<AgentRole, string> = {
[AgentRole.SYSTEM]: "System",
[AgentRole.CEO]: "CEO",
[AgentRole.PRODUCT_OWNER]: "Product Owner",
[AgentRole.HEAD_MARKETING]: "Head Marketing",
[AgentRole.AUDITOR]: "Auditor",
[AgentRole.MAIN_PM]: "Main PM",
[AgentRole.CELL_PM]: "Cell PM",
[AgentRole.DEVELOPER]: "Developer",
[AgentRole.QA]: "QA",
[AgentRole.DOCUMENTER]: "Documenter",
};
export function AgentSelector({
value,
onChange,
placeholder = "Select agent...",
filterByTeam,
filterByRoles,
disabled = false,
allowClear = true,
}: AgentSelectorProps) {
const { data: agents = [], isLoading } = useAgentDefinitions();
// Group agents by team
const groupedAgents = useMemo(() => {
let filtered = agents;
// Apply team filter - also match by role for Board and Main PM
if (filterByTeam) {
filtered = filtered.filter((a) => {
// Direct team match
if (a.team === filterByTeam) return true;
// For Board team, also include board-level roles
if (filterByTeam === Team.BOARD && (
a.role === AgentRole.PRODUCT_OWNER ||
a.role === AgentRole.HEAD_MARKETING ||
a.role === AgentRole.AUDITOR
)) return true;
// For Main PM team, also include Main PM role
if (filterByTeam === Team.MAIN_PM && a.role === AgentRole.MAIN_PM) return true;
return false;
});
}
// Apply role filter
if (filterByRoles && filterByRoles.length > 0) {
filtered = filtered.filter((a) => a.role && filterByRoles.includes(a.role));
}
// Group by team following org hierarchy
const groups: Record<string, typeof filtered> = {
board: [],
main_pm: [],
backend: [],
frontend: [],
ux_ui: [],
marketing: [],
};
for (const agent of filtered) {
if (agent.team === Team.BOARD ||
agent.role === AgentRole.PRODUCT_OWNER ||
agent.role === AgentRole.HEAD_MARKETING ||
agent.role === AgentRole.AUDITOR) {
groups.board.push(agent);
} else if (agent.team === Team.MAIN_PM || agent.role === AgentRole.MAIN_PM) {
groups.main_pm.push(agent);
} else if (agent.team === Team.BACKEND) {
groups.backend.push(agent);
} else if (agent.team === Team.FRONTEND) {
groups.frontend.push(agent);
} else if (agent.team === Team.UX_UI) {
groups.ux_ui.push(agent);
} else if (agent.team === Team.MARKETING) {
groups.marketing.push(agent);
}
// Note: Agents without team are not grouped - they remain ungrouped
// The orchestrator handles automatic routing for unassigned tasks
}
return groups;
}, [agents, filterByTeam, filterByRoles]);
// Find selected agent for display (resolve UUID to slug if needed)
const selectedAgent = useMemo(() => {
if (!value) return null;
const resolvedValue = resolveToSlug(value);
return agents.find((a) => a.id === resolvedValue || a.id === value);
}, [agents, value]);
const handleValueChange = (newValue: string) => {
if (newValue === "__clear__") {
onChange(null);
} else {
onChange(newValue);
}
};
// Resolve value to slug for proper Select matching
const selectValue = value ? resolveToSlug(value) || value : "";
return (
<Select
value={selectValue}
onValueChange={handleValueChange}
disabled={disabled || isLoading}
>
<SelectTrigger className="w-full">
<SelectValue placeholder={placeholder}>
{selectedAgent ? (
<div className="flex items-center gap-2">
<User className="h-4 w-4" />
<span>{selectedAgent.name}</span>
{selectedAgent.role && (
<Badge variant="outline" className="text-xs">
{ROLE_LABELS[selectedAgent.role] || selectedAgent.role}
</Badge>
)}
</div>
) : (
placeholder
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{allowClear && value && (
<SelectItem value="__clear__" className="text-muted-foreground">
<span className="flex items-center gap-2">
<Users className="h-4 w-4" />
Unassigned
</span>
</SelectItem>
)}
{/* Board */}
{groupedAgents.board.length > 0 && (
<SelectGroup>
<SelectLabel>Board</SelectLabel>
{groupedAgents.board.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
<div className="flex items-center gap-2">
<span>{agent.name}</span>
{agent.role && (
<Badge variant="secondary" className="text-xs">
{ROLE_LABELS[agent.role] || agent.role}
</Badge>
)}
</div>
</SelectItem>
))}
</SelectGroup>
)}
{/* Main PM */}
{groupedAgents.main_pm.length > 0 && (
<SelectGroup>
<SelectLabel>Main PM</SelectLabel>
{groupedAgents.main_pm.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
<div className="flex items-center gap-2">
<span>{agent.name}</span>
{agent.role && (
<Badge variant="secondary" className="text-xs">
{ROLE_LABELS[agent.role] || agent.role}
</Badge>
)}
</div>
</SelectItem>
))}
</SelectGroup>
)}
{/* Backend */}
{groupedAgents.backend.length > 0 && (
<SelectGroup>
<SelectLabel>Backend Team</SelectLabel>
{groupedAgents.backend.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
<div className="flex items-center gap-2">
<span>{agent.name}</span>
{agent.role && (
<Badge variant="secondary" className="text-xs">
{ROLE_LABELS[agent.role] || agent.role}
</Badge>
)}
</div>
</SelectItem>
))}
</SelectGroup>
)}
{/* Frontend */}
{groupedAgents.frontend.length > 0 && (
<SelectGroup>
<SelectLabel>Frontend Team</SelectLabel>
{groupedAgents.frontend.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
<div className="flex items-center gap-2">
<span>{agent.name}</span>
{agent.role && (
<Badge variant="secondary" className="text-xs">
{ROLE_LABELS[agent.role] || agent.role}
</Badge>
)}
</div>
</SelectItem>
))}
</SelectGroup>
)}
{/* UX/UI */}
{groupedAgents.ux_ui.length > 0 && (
<SelectGroup>
<SelectLabel>UX/UI Team</SelectLabel>
{groupedAgents.ux_ui.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
<div className="flex items-center gap-2">
<span>{agent.name}</span>
{agent.role && (
<Badge variant="secondary" className="text-xs">
{ROLE_LABELS[agent.role] || agent.role}
</Badge>
)}
</div>
</SelectItem>
))}
</SelectGroup>
)}
{/* Marketing */}
{groupedAgents.marketing.length > 0 && (
<SelectGroup>
<SelectLabel>Marketing Team</SelectLabel>
{groupedAgents.marketing.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
<div className="flex items-center gap-2">
<span>{agent.name}</span>
{agent.role && (
<Badge variant="secondary" className="text-xs">
{ROLE_LABELS[agent.role] || agent.role}
</Badge>
)}
</div>
</SelectItem>
))}
</SelectGroup>
)}
</SelectContent>
</Select>
);
}
@@ -0,0 +1,61 @@
import { Badge } from "@/components/ui/badge";
import { Clock, RefreshCw, Activity, AlertTriangle, Square } from "lucide-react";
// Agent states as returned by backend orchestrator
type AgentStateString =
| "idle"
| "starting"
| "ready"
| "running"
| "waiting_long"
| "error"
| "stopped"
| "terminated";
const stateColors: Record<string, string> = {
idle: "bg-gray-500",
starting: "bg-yellow-500",
ready: "bg-blue-500",
running: "bg-green-500",
waiting_long: "bg-orange-500",
error: "bg-red-500",
stopped: "bg-gray-400",
terminated: "bg-gray-600",
};
const stateIcons: Record<string, React.ReactNode> = {
idle: <Clock className="h-4 w-4" />,
starting: <RefreshCw className="h-4 w-4 animate-spin" />,
ready: <Activity className="h-4 w-4" />,
running: <Activity className="h-4 w-4" />,
waiting_long: <AlertTriangle className="h-4 w-4" />,
error: <AlertTriangle className="h-4 w-4" />,
stopped: <Square className="h-4 w-4" />,
terminated: <Square className="h-4 w-4" />,
};
interface AgentStateBadgeProps {
state: AgentStateString | string;
showIcon?: boolean;
size?: "sm" | "md" | "lg";
}
export function AgentStateBadge({ state, showIcon = true, size = "md" }: AgentStateBadgeProps) {
const sizeClasses = {
sm: "text-xs px-2 py-0.5",
md: "text-sm px-2.5 py-0.5",
lg: "text-lg px-3 py-1",
};
const color = stateColors[state] || "bg-gray-400";
const icon = stateIcons[state] || <Square className="h-4 w-4" />;
return (
<Badge className={`${color} text-white ${sizeClasses[size]}`}>
{showIcon && <span className="mr-1">{icon}</span>}
{state.replace(/_/g, " ")}
</Badge>
);
}
export { stateColors, stateIcons };
@@ -0,0 +1,73 @@
import Link from "next/link";
import { formatDistanceToNow } from "date-fns";
import { AgentStatusResponse } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Activity, FileText, Clock, AlertCircle } from "lucide-react";
import { AgentStateBadge } from "./agent-state-badge";
interface AgentStatusCardsProps {
agent: AgentStatusResponse;
}
export function AgentStatusCards({ agent }: AgentStatusCardsProps) {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">State</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<AgentStateBadge state={agent.state} size="lg" />
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Current Task</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{agent.task_id ? (
<Link href={"/tasks/" + agent.task_id} className="text-blue-500 hover:underline">
{agent.task_id.slice(0, 8)}...
</Link>
) : (
<span className="text-muted-foreground">No task assigned</span>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Started At</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{agent.started_at ? (
<span>{formatDistanceToNow(new Date(agent.started_at))} ago</span>
) : (
<span className="text-muted-foreground">Not started</span>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Error Count</CardTitle>
<AlertCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<span className={agent.error_count > 0 ? "text-red-600 font-semibold" : ""}>
{agent.error_count}
</span>
{agent.waiting_for && (
<p className="text-xs text-yellow-600 mt-1 truncate">
Waiting: {agent.waiting_for}
</p>
)}
</CardContent>
</Card>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
export { AgentStateBadge, stateColors, stateIcons } from "./agent-state-badge";
export { AgentCard } from "./agent-card";
export { AgentGrid } from "./agent-grid";
export { AgentStatusCards } from "./agent-status-cards";
export { SpawnAgentDialog } from "./spawn-agent-dialog";
export { ResolveWaitDialog } from "./resolve-wait-dialog";
export { AgentStreamViewer } from "./stream-viewer";
export { OrchestratorStatusCards } from "./orchestrator-status";
export { WaitingAgentsAlert } from "./waiting-agents-alert";
@@ -0,0 +1,77 @@
import { OrchestratorStatus as OrchestratorStatusType } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Server, Users, Clock, Activity } from "lucide-react";
interface OrchestratorStatusCardsProps {
status: OrchestratorStatusType | undefined;
isLoading: boolean;
}
export function OrchestratorStatusCards({ status, isLoading }: OrchestratorStatusCardsProps) {
// Calculate running agents from by_state
const runningCount = status?.by_state?.running || 0;
const readyCount = status?.by_state?.ready || 0;
const activeCount = runningCount + readyCount;
const isRunning = status && status.total_agents > 0;
return (
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Orchestrator</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-24" />
) : (
<Badge className={isRunning ? "bg-green-500" : "bg-red-500"}>
{isRunning ? "Running" : "Stopped"}
</Badge>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Agents</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-12" />
) : (
<div className="text-2xl font-bold">{status?.total_agents || 0}</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-12" />
) : (
<div className="text-2xl font-bold">{activeCount}</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Waiting</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-12" />
) : (
<div className="text-2xl font-bold">{status?.waiting_count || 0}</div>
)}
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,82 @@
"use client";
import { useState } from "react";
import { useResolveWait } from "@/hooks/use-agents";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogDescription,
} from "@/components/ui/dialog";
import { Send } from "lucide-react";
import { toast } from "sonner";
interface ResolveWaitDialogProps {
agentId: string;
}
export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
const [open, setOpen] = useState(false);
const [resolution, setResolution] = useState("");
const resolveWait = useResolveWait();
const handleResolve = async () => {
if (!resolution.trim()) {
toast.error("Please provide a resolution");
return;
}
try {
await resolveWait.mutateAsync({ agentId, resolution });
toast.success("Resolution sent to agent");
setOpen(false);
setResolution("");
} catch {
toast.error("Failed to resolve wait");
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Send className="h-4 w-4 mr-2" />
Resolve Wait
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Resolve Agent Wait</DialogTitle>
<DialogDescription>
Provide context or instructions to help the agent continue.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="resolution">Resolution Message</Label>
<Textarea
id="resolution"
value={resolution}
onChange={(e) => setResolution(e.target.value)}
placeholder="Provide the information or decision the agent needs to continue..."
rows={4}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button onClick={handleResolve} disabled={resolveWait.isPending}>
{resolveWait.isPending ? "Sending..." : "Send Resolution"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,104 @@
"use client";
import { useState } from "react";
import { useSpawnAgent } from "@/hooks/use-agents";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogDescription,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Play } from "lucide-react";
import { toast } from "sonner";
interface SpawnAgentDialogProps {
agentId: string;
agentName: string;
trigger?: React.ReactNode;
}
export function SpawnAgentDialog({ agentId, agentName, trigger }: SpawnAgentDialogProps) {
const [open, setOpen] = useState(false);
const [taskId, setTaskId] = useState("");
const [initialPrompt, setInitialPrompt] = useState("");
const spawnAgent = useSpawnAgent();
const handleSpawn = async () => {
try {
await spawnAgent.mutateAsync({
agentId,
request: {
task_id: taskId || undefined,
initial_prompt: initialPrompt || undefined,
},
});
toast.success(`Agent ${agentName} spawned successfully`);
setOpen(false);
resetForm();
} catch {
toast.error("Failed to spawn agent");
}
};
const resetForm = () => {
setTaskId("");
setInitialPrompt("");
};
const defaultTrigger = (
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Play className="h-4 w-4 mr-2" />
Spawn
</DropdownMenuItem>
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{trigger || defaultTrigger}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Spawn {agentName}</DialogTitle>
<DialogDescription>
Start this agent with optional task assignment and initial prompt.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="taskId">Task ID (optional)</Label>
<Input
id="taskId"
value={taskId}
onChange={(e) => setTaskId(e.target.value)}
placeholder="UUID of task to assign"
/>
</div>
<div className="space-y-2">
<Label htmlFor="initialPrompt">Initial Prompt (optional)</Label>
<Input
id="initialPrompt"
value={initialPrompt}
onChange={(e) => setInitialPrompt(e.target.value)}
placeholder="Initial instructions for the agent"
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button onClick={handleSpawn} disabled={spawnAgent.isPending}>
{spawnAgent.isPending ? "Spawning..." : "Spawn Agent"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,101 @@
"use client";
import { useEffect, useRef } from "react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useAgentStream, ConnectionState } from "@/hooks/use-websocket";
import { Wifi, WifiOff, Loader2, Trash2 } from "lucide-react";
interface AgentStreamViewerProps {
agentId: string;
agentName?: string;
}
const stateColors: Record<ConnectionState, string> = {
connected: "bg-green-500",
connecting: "bg-yellow-500",
reconnecting: "bg-orange-500",
disconnected: "bg-gray-500",
};
const stateLabels: Record<ConnectionState, string> = {
connected: "Connected",
connecting: "Connecting...",
reconnecting: "Reconnecting...",
disconnected: "Disconnected",
};
export function AgentStreamViewer({ agentId, agentName }: AgentStreamViewerProps) {
const {
state,
streamOutput,
streamChunks,
clearMessages,
isConnected,
isConnecting
} = useAgentStream(agentId);
const outputRef = useRef<HTMLPreElement>(null);
// Auto-scroll to bottom when new content arrives
useEffect(() => {
if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight;
}
}, [streamOutput]);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
Agent Output Stream
{isConnecting ? (
<Loader2 className="h-4 w-4 animate-spin text-yellow-500" />
) : isConnected ? (
<Wifi className="h-4 w-4 text-green-500" />
) : (
<WifiOff className="h-4 w-4 text-gray-500" />
)}
</CardTitle>
<CardDescription>
Real-time LLM output from {agentName || agentId}
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Badge className={stateColors[state] + " text-white"}>
{stateLabels[state]}
</Badge>
{streamChunks.length > 0 && (
<Button variant="ghost" size="icon" onClick={clearMessages}>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent>
<pre
ref={outputRef}
className="bg-slate-950 text-slate-50 rounded-lg p-4 h-96 overflow-auto font-mono text-sm whitespace-pre-wrap"
>
{streamOutput || (
<span className="text-slate-500">
{isConnected
? "Waiting for agent output..."
: isConnecting
? "Connecting to agent stream..."
: "Agent stream disconnected"}
</span>
)}
</pre>
<div className="flex justify-between items-center mt-2 text-sm text-muted-foreground">
<span>{streamChunks.length} chunks received</span>
<span>{streamOutput.length} characters</span>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,40 @@
import Link from "next/link";
import { WaitingAgent } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { AlertTriangle } from "lucide-react";
import { getAgentDisplayName } from "@/lib/agent-utils";
interface WaitingAgentsAlertProps {
waitingAgents: WaitingAgent[];
}
export function WaitingAgentsAlert({ waitingAgents }: WaitingAgentsAlertProps) {
if (waitingAgents.length === 0) return null;
return (
<Card className="border-orange-500/50">
<CardHeader>
<CardTitle className="text-orange-500 flex items-center gap-2">
<AlertTriangle className="h-5 w-5" />
Agents Waiting for Input
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{waitingAgents.map((agent) => (
<div key={agent.agent_id} className="flex items-center justify-between p-2 bg-muted rounded">
<div>
<span className="font-medium">{getAgentDisplayName(agent.agent_id)}</span>
<span className="text-muted-foreground ml-2">waiting for: {agent.waiting_for}</span>
</div>
<Button variant="outline" size="sm" asChild>
<Link href={"/agents/" + agent.agent_id}>Resolve</Link>
</Button>
</div>
))}
</div>
</CardContent>
</Card>
);
}