feat: Council - agent teams management

This commit is contained in:
root
2026-02-16 13:19:21 +00:00
parent eaaa556448
commit d09d6985ca
3 changed files with 406 additions and 169 deletions
+188
View File
@@ -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>
);
}