From d09d6985ca437ce86a754ac5631cea3c314ad2f8 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 16 Feb 2026 13:19:21 +0000 Subject: [PATCH] feat: Council - agent teams management --- components/council/Council.tsx | 188 ++++++++++ .../MissionControlDashboard.tsx | 320 +++++++++--------- lib/council/types.ts | 67 ++++ 3 files changed, 406 insertions(+), 169 deletions(-) create mode 100644 components/council/Council.tsx create mode 100644 lib/council/types.ts diff --git a/components/council/Council.tsx b/components/council/Council.tsx new file mode 100644 index 0000000..43fb7ae --- /dev/null +++ b/components/council/Council.tsx @@ -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(defaultTeams); + const [selectedTeam, setSelectedTeam] = useState(null); + const [runningTask, setRunningTask] = useState(""); + const [agentOutputs, setAgentOutputs] = useState>({}); + + // 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 ( +
+ {/* Header */} +
+

+ šŸ›ļø Council +

+

+ Manage AI agent teams for each project. I'm the boss — I delegate tasks and oversee execution. +

+
+ + {/* Team Grid */} +
+ {teams.map((team) => ( + + ))} +
+ + {/* Selected Team Detail */} + {currentTeam && ( +
+
+
+ {currentTeam.icon} +
+

{currentTeam.name}

+

{currentTeam.description}

+
+
+ +
+ + {/* Agents */} +
+ {currentTeam.agents.map((agent) => ( +
+
+
+ + {agent.status.toUpperCase()} + + {agent.name} + — {agent.role} +
+ {agent.lastRun && ( + Last: {new Date(agent.lastRun).toLocaleTimeString()} + )} +
+ +

{agent.description}

+ + {/* Task Input */} +
+ 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)} + /> + +
+ + {/* Output */} + {agentOutputs[agent.id] && ( +
+ {agentOutputs[agent.id]} +
+ )} +
+ ))} +
+
+ )} + + {/* Horus Boss Note */} +
+

+ šŸ‘ļø Note: 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. +

+
+
+ ); +} diff --git a/components/mission-control/MissionControlDashboard.tsx b/components/mission-control/MissionControlDashboard.tsx index ef7e0b3..a4bee94 100644 --- a/components/mission-control/MissionControlDashboard.tsx +++ b/components/mission-control/MissionControlDashboard.tsx @@ -5,6 +5,7 @@ import { useMissionControl } from "@/lib/mission-control/store"; import { TaskStatus } from "@/lib/mission-control/types"; import VoiceChat from "./VoiceChat"; import AIManagement from "@/components/ai-management/AIManagement"; +import Council from "@/components/council/Council"; interface SidebarItem { id: string; @@ -33,7 +34,8 @@ const sidebarCategories: SidebarCategory[] = [ { id: "voice", name: "Voice Chat", icon: "šŸŽ¤", category: "chat" }, ]}, { id: "council", name: "Council", icon: "šŸ›ļø", items: [ - { id: "ai-settings", name: "AI Settings", icon: "šŸ¤–", category: "council" }, + { id: "teams", name: "Agent Teams", icon: "šŸ‘„", category: "council" }, + { id: "ai-settings", name: "AI Settings", icon: "šŸ¤–", category: "council-settings" }, ]}, { id: "calendar", name: "Calendar", icon: "šŸ“…", items: [ { id: "brief", name: "Morning Brief", icon: "ā˜€ļø", category: "calendar" }, @@ -61,7 +63,6 @@ export default function MissionControlDashboard() { const [newTaskTitle, setNewTaskTitle] = useState(""); const [newTaskProject, setNewTaskProject] = useState("sitemente"); - // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "/" && !e.ctrlKey && !e.metaKey) { @@ -84,27 +85,18 @@ export default function MissionControlDashboard() { return () => window.removeEventListener("keydown", handleKeyDown); }, []); - // Find selected item let selectedCategory = sidebarCategories.find(c => c.items.some(i => i.id === selectedItem)); let currentItem = selectedCategory?.items.find(i => i.id === selectedItem); const projectTasks = currentItem?.category === "projects" ? getTasksByProject(selectedItem as any) : tasks; const filteredTasks = filter === "all" ? projectTasks : projectTasks.filter((t) => t.status === filter); - const searchedTasks = searchQuery - ? filteredTasks.filter(t => t.title.toLowerCase().includes(searchQuery.toLowerCase())) - : filteredTasks; + const searchedTasks = searchQuery ? filteredTasks.filter(t => t.title.toLowerCase().includes(searchQuery.toLowerCase())) : filteredTasks; const progress = currentItem?.category === "projects" ? getProjectProgress(selectedItem as any) : 0; - const toggleCategory = (catId: string) => { - setExpandedCategories(prev => prev.includes(catId) ? prev.filter(id => id !== catId) : [...prev, catId]); - }; - + const toggleCategory = (catId: string) => setExpandedCategories(prev => prev.includes(catId) ? prev.filter(id => id !== catId) : [...prev, catId]); const collapseAll = () => setExpandedCategories([]); - - // Today's focus const todayTasks = projectTasks.filter(t => t.priority === "critical" || t.status === "in_progress").slice(0, 3); - // Export function const exportTasks = () => { const data = JSON.stringify(projectTasks, null, 2); const blob = new Blob([data], { type: "application/json" }); @@ -115,7 +107,6 @@ export default function MissionControlDashboard() { a.click(); }; - // Add task const handleAddTask = () => { if (!newTaskTitle.trim()) return; addTask({ title: newTaskTitle, description: "", status: "todo", priority: "medium", project: newTaskProject as any }); @@ -123,164 +114,155 @@ export default function MissionControlDashboard() { setNewTaskTitle(""); }; - // Render functions - const renderSidebar = () => ( - - ); - - const renderHeader = () => ( -
-
- -
-

{currentItem?.icon}{currentItem?.name}

-

{currentItem?.category === "projects" ? `${progress}% complete` : `${tasks.length} total tasks`}

-
-
- {currentItem?.category === "projects" && ( -
- - - - -
- )} -
- ); - - const renderContent = () => { - const category = currentItem?.category; - if (category === "chat") return
; - if (category === "council") return
; - if (category === "calendar") return ( -
-
-

šŸ“… Morning Brief

-

Daily intelligence at 6am CET

- ā˜€ļø Open Calendar -
- -
- ); - if (category === "memory") return ( -
-

🧠 Memory & Logs

-

Session history and daily logs

-

Memory is stored in:

  • • localStorage (browser)
  • • GitHub repo (daily commits)
  • • MEMORY.md (curated)
-
- ); - return renderTasksView(); - }; - - const renderTasksView = () => ( - <> - {todayTasks.length > 0 && ( -
-

šŸŽÆ TODAY'S FOCUS

-
{todayTasks.map(task => ( -
- - {task.title} -
- ))}
-
- )} -
-
- šŸ” - setSearchQuery(e.target.value)} placeholder="Search tasks... (press /)" className="w-full bg-white/10 border border-white/20 rounded-lg pl-9 pr-4 py-2 text-sm placeholder:text-white/40 focus:outline-none focus:border-brand-pink" /> -
- - -
- {showAddTask && ( -
- setNewTaskTitle(e.target.value)} placeholder="New task title..." className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-sm placeholder:text-white/40 focus:outline-none focus:border-brand-pink mb-3" autoFocus onKeyDown={(e) => e.key === "Enter" && handleAddTask()} /> -
- - - -
-
- )} -
- {(["todo", "in_progress", "done", "blocked"] as TaskStatus[]).map((status) => { - const count = projectTasks.filter((t) => t.status === status).length; - const config = statusConfig[status]; - return ( - - ); - })} -
-
-
-
-

{currentItem?.name || "Tasks"}{searchQuery && ({searchedTasks.length} results)}

-

{filteredTasks.filter((t) => t.status === "done").length} / {filteredTasks.length} done

-
-
-
- {searchedTasks.map((task) => { - const config = statusConfig[task.status]; - return ( -
- -

{task.title}

- {task.priority === "critical" && CRITICAL} - -
- ); - })} -
- {searchedTasks.length === 0 &&
No tasks match the current filter.
} -
- - ); - return (
- {renderSidebar()} + +
- {renderHeader()} - {renderContent()} +
+
+ +
+

{currentItem?.icon}{currentItem?.name}

+

{currentItem?.category === "projects" ? `${progress}% complete` : `${tasks.length} total tasks`}

+
+
+ {currentItem?.category === "projects" && ( +
+ + + + +
+ )} +
+ + {currentItem?.category === "chat" &&
} + + {currentItem?.category === "council" && currentItem?.id === "teams" &&
} + + {currentItem?.category === "council-settings" &&
} + + {currentItem?.category === "calendar" && ( +
+
+

šŸ“… Morning Brief

+

Daily intelligence at 6am CET

+ ā˜€ļø Open Calendar +
+ +
+ )} + + {currentItem?.category === "memory" && ( +
+

🧠 Memory & Logs

+

Session history and daily logs

+

Memory is stored in:

  • • localStorage (browser)
  • • GitHub repo (daily commits)
  • • MEMORY.md (curated)
+
+ )} + + {currentItem?.category === "projects" && ( + <> + {todayTasks.length > 0 && ( +
+

šŸŽÆ TODAY'S FOCUS

+
{todayTasks.map(task => ( +
+ + {task.title} +
+ ))}
+
+ )} +
+
+ šŸ” + setSearchQuery(e.target.value)} placeholder="Search tasks... (press /)" className="w-full bg-white/10 border border-white/20 rounded-lg pl-9 pr-4 py-2 text-sm placeholder:text-white/40 focus:outline-none focus:border-brand-pink" /> +
+ + +
+ {showAddTask && ( +
+ setNewTaskTitle(e.target.value)} placeholder="New task title..." className="w-full bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-sm placeholder:text-white/40 focus:outline-none focus:border-brand-pink mb-3" autoFocus onKeyDown={(e) => e.key === "Enter" && handleAddTask()} /> +
+ + + +
+
+ )} +
+ {(["todo", "in_progress", "done", "blocked"] as TaskStatus[]).map((status) => { + const count = projectTasks.filter((t) => t.status === status).length; + const config = statusConfig[status]; + return ( + + ); + })} +
+
+
+
+

{currentItem?.name}{searchQuery && ({searchedTasks.length} results)}

+

{filteredTasks.filter((t) => t.status === "done").length} / {filteredTasks.length} done

+
+
+
+ {searchedTasks.map((task) => ( +
+ +

{task.title}

+ {task.priority === "critical" && CRITICAL} + +
+ ))} +
+ {searchedTasks.length === 0 &&
No tasks match.
} +
+ + )}
); diff --git a/lib/council/types.ts b/lib/council/types.ts new file mode 100644 index 0000000..c2dbf59 --- /dev/null +++ b/lib/council/types.ts @@ -0,0 +1,67 @@ +// Agent Types for Council + +export interface Agent { + id: string; + name: string; + role: string; + description: string; + status: "idle" | "working" | "completed" | "error"; + project: string; + lastRun?: string; + currentTask?: string; +} + +export interface AgentTeam { + id: string; + name: string; + icon: string; + description: string; + agents: Agent[]; +} + +export const defaultTeams: AgentTeam[] = [ + { + id: "sitemente-squad", + name: "SiteMente Squad", + icon: "🌐", + description: "Building and growing SiteMente B2B platform", + agents: [ + { id: "sm-architect", name: "Architect", role: "PM & Architecture", description: "Planning, architecture, task breakdown", status: "idle", project: "sitemente" }, + { id: "sm-frontend", name: "Frontend Dev", role: "UI/UX Implementation", description: "React, Next.js, components", status: "idle", project: "sitemente" }, + { id: "sm-ai", name: "AI Engineer", role: "LLM & Voice Integration", description: "Gemini, voice agents, widgets", status: "idle", project: "sitemente" }, + { id: "sm-seo", name: "SEO Specialist", role: "Content & Copy", description: "SEO, content, marketing copy", status: "idle", project: "sitemente" }, + ], + }, + { + id: "holacompi-squad", + name: "HolaCompi Squad", + icon: "šŸ¤", + description: "Consumer AI ally (paused until revenue)", + agents: [ + { id: "hc-product", name: "Product Manager", role: "Flows & UX", description: "User flows, product decisions", status: "idle", project: "holacompi" }, + { id: "hc telephony", name: "Telephony Engineer", role: "Voice & Telco", description: "Telnyx, Vapi, phone lines", status: "idle", project: "holacompi" }, + { id: "hc-voice", name: "Voice UX", role: "Conversation Design", description: "Dialogues, prompts, voice UX", status: "idle", project: "holacompi" }, + ], + }, + { + id: "infrastructure-team", + name: "Infrastructure", + icon: "šŸ”§", + description: "Security, backups, and system ops", + agents: [ + { id: "infra-sec", name: "Security Lead", role: "Hardening & Audits", description: "UFW, SSH, security audits", status: "idle", project: "infrastructure" }, + { id: "infra-backup", name: "Backup Manager", role: "Backup & Recovery", description: "Auto backups, cloud sync", status: "idle", project: "infrastructure" }, + ], + }, +]; + +export const agentRoles = [ + { id: "architect", name: "Architect / PM", icon: "šŸ“‹" }, + { id: "frontend", name: "Frontend Dev", icon: "šŸŽØ" }, + { id: "backend", name: "Backend Dev", icon: "āš™ļø" }, + { id: "ai-engineer", name: "AI Engineer", icon: "šŸ¤–" }, + { id: "seo", name: "SEO / Copy", icon: "šŸ“" }, + { id: "product", name: "Product Manager", icon: "šŸ“¦" }, + { id: "security", name: "Security", icon: "šŸ›”ļø" }, + { id: "devops", name: "DevOps", icon: "šŸš€" }, +];