feat: Council - agent teams management
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Agent, AgentTeam, defaultTeams } from "@/lib/council/types";
|
||||
|
||||
const STORAGE_KEY = "horus:council";
|
||||
|
||||
export default function Council() {
|
||||
const [teams, setTeams] = useState<AgentTeam[]>(defaultTeams);
|
||||
const [selectedTeam, setSelectedTeam] = useState<string | null>(null);
|
||||
const [runningTask, setRunningTask] = useState<string>("");
|
||||
const [agentOutputs, setAgentOutputs] = useState<Record<string, string>>({});
|
||||
|
||||
// Load from localStorage
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
try {
|
||||
setTeams(JSON.parse(saved));
|
||||
} catch {}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Save changes
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(teams));
|
||||
}, [teams]);
|
||||
|
||||
const getStatusColor = (status: Agent["status"]) => {
|
||||
switch (status) {
|
||||
case "working": return "text-yellow-400 bg-yellow-400/20";
|
||||
case "completed": return "text-green-400 bg-green-400/20";
|
||||
case "error": return "text-red-400 bg-red-400/20";
|
||||
default: return "text-white/50 bg-white/10";
|
||||
}
|
||||
};
|
||||
|
||||
const spawnAgent = async (agent: Agent, task: string) => {
|
||||
if (!task.trim()) return;
|
||||
|
||||
// Update agent status
|
||||
setTeams(prev => prev.map(team => ({
|
||||
...team,
|
||||
agents: team.agents.map(a => a.id === agent.id ? { ...a, status: "working" as const, currentTask: task } : a)
|
||||
})));
|
||||
setRunningTask("");
|
||||
|
||||
// Simulate agent work (in reality this would call actual agents)
|
||||
const output = `[🤖 ${agent.name}] Starting task: "${task}"\n\n`;
|
||||
setAgentOutputs(prev => ({ ...prev, [agent.id]: output + "⏳ Processing...\n" }));
|
||||
|
||||
// Simulate completion after delay
|
||||
setTimeout(() => {
|
||||
const result = output + `✅ Task completed: ${task}\n\n💡 Result: Task processed successfully.`;
|
||||
setAgentOutputs(prev => ({ ...prev, [agent.id]: result }));
|
||||
|
||||
setTeams(prev => prev.map(team => ({
|
||||
...team,
|
||||
agents: team.agents.map(a => a.id === agent.id ? {
|
||||
...a,
|
||||
status: "completed" as const,
|
||||
lastRun: new Date().toISOString(),
|
||||
currentTask: undefined
|
||||
} : a)
|
||||
})));
|
||||
}, 3000 + Math.random() * 2000);
|
||||
};
|
||||
|
||||
const currentTeam = teams.find(t => t.id === selectedTeam);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||
🏛️ Council
|
||||
</h2>
|
||||
<p className="text-white/60 mt-1">
|
||||
Manage AI agent teams for each project. I'm the boss — I delegate tasks and oversee execution.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Team Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{teams.map((team) => (
|
||||
<button
|
||||
key={team.id}
|
||||
onClick={() => setSelectedTeam(team.id)}
|
||||
className={`p-4 rounded-xl border text-left transition ${
|
||||
selectedTeam === team.id
|
||||
? "border-brand-pink bg-brand-pink/10"
|
||||
: "border-white/10 bg-white/5 hover:border-white/30"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="text-2xl">{team.icon}</span>
|
||||
<div>
|
||||
<p className="font-semibold">{team.name}</p>
|
||||
<p className="text-xs text-white/50">{team.agents.length} agents</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-white/60">{team.description}</p>
|
||||
<div className="mt-3 flex gap-1">
|
||||
{team.agents.slice(0, 4).map(a => (
|
||||
<span key={a.id} className={`w-2 h-2 rounded-full ${getStatusColor(a.status).split(' ')[1].replace('/', '-')}`} />
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Selected Team Detail */}
|
||||
{currentTeam && (
|
||||
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">{currentTeam.icon}</span>
|
||||
<div>
|
||||
<h3 className="font-semibold">{currentTeam.name}</h3>
|
||||
<p className="text-sm text-white/50">{currentTeam.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setSelectedTeam(null)} className="text-white/50 hover:text-white">✕</button>
|
||||
</div>
|
||||
|
||||
{/* Agents */}
|
||||
<div className="space-y-3">
|
||||
{currentTeam.agents.map((agent) => (
|
||||
<div key={agent.id} className="p-4 rounded-lg border border-white/10 bg-white/5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${getStatusColor(agent.status)}`}>
|
||||
{agent.status.toUpperCase()}
|
||||
</span>
|
||||
<span className="font-medium">{agent.name}</span>
|
||||
<span className="text-white/50">— {agent.role}</span>
|
||||
</div>
|
||||
{agent.lastRun && (
|
||||
<span className="text-xs text-white/40">Last: {new Date(agent.lastRun).toLocaleTimeString()}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-white/60 mb-3">{agent.description}</p>
|
||||
|
||||
{/* Task Input */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={runningTask}
|
||||
onChange={(e) => setRunningTask(e.target.value)}
|
||||
placeholder={`Ask ${agent.name} to...`}
|
||||
className="flex-1 bg-white/10 border border-white/20 rounded-lg px-3 py-2 text-sm placeholder:text-white/40 focus:outline-none focus:border-brand-pink"
|
||||
onKeyDown={(e) => e.key === "Enter" && spawnAgent(agent, runningTask)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => spawnAgent(agent, runningTask)}
|
||||
disabled={agent.status === "working" || !runningTask.trim()}
|
||||
className="px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium disabled:opacity-50 hover:bg-[#ff7bc0] transition"
|
||||
>
|
||||
🚀 Run
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Output */}
|
||||
{agentOutputs[agent.id] && (
|
||||
<div className="mt-3 p-3 rounded-lg bg-black/30 text-sm font-mono text-white/80 whitespace-pre-wrap max-h-40 overflow-y-auto">
|
||||
{agentOutputs[agent.id]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Horus Boss Note */}
|
||||
<div className="p-4 rounded-xl border border-brand-pink/30 bg-brand-pink/10">
|
||||
<p className="text-sm text-brand-pink">
|
||||
👁️ <strong>Note:</strong> I coordinate these agents. They run as sub-tasks within my sessions.
|
||||
In production, they'd be separate AI processes. Currently simulating — real agent spawning requires
|
||||
the OpenClaw sub-agent system to be configured.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user