diff --git a/src/components/graph/assignment-panel.tsx b/src/components/graph/assignment-panel.tsx index 87419c5..0f0e65d 100644 --- a/src/components/graph/assignment-panel.tsx +++ b/src/components/graph/assignment-panel.tsx @@ -1,14 +1,17 @@ "use client"; import React, { useState, useMemo } from 'react'; -import { Zap, Users, Blocks, FileCode2, Loader2, UserPlus, Clock, AlertCircle } from 'lucide-react'; +import { Zap, Users, FileCode2, Loader2, UserPlus, Clock, AlertCircle, ChevronDown, ChevronRight, Blocks, Layers } from 'lucide-react'; import { ArchetypeInspector } from '../swarm/archetype-inspector'; import { TemplateInspector } from '../swarm/template-inspector'; +import { ArchetypePicker } from '../swarm/archetype-picker'; +import { TemplatePicker } from '../swarm/template-picker'; import { useArchetypes } from '../../hooks/use-archetypes'; import { useTemplates } from '../../hooks/use-templates'; import { useGraphAnalysis } from '../../hooks/use-graph-analysis'; import type { BeadIssue } from '../../lib/types'; -import type { AgentArchetype } from '../../lib/types-swarm'; +import type { AgentArchetype, SwarmTemplate } from '../../lib/types-swarm'; +import { getArchetypeDisplayChar } from '../../lib/utils'; export interface AssignmentPanelProps { selectedIssue: BeadIssue | null; @@ -34,24 +37,47 @@ function truncateTitle(title: string, maxLength: number = 30): string { return title.slice(0, maxLength - 3) + '...'; } +function getTemplateId(issue: BeadIssue): string | null { + if (issue.metadata?.templateId && typeof issue.metadata.templateId === 'string') { + return issue.metadata.templateId; + } + return issue.templateId; +} + export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }: AssignmentPanelProps) { const [inspectingArchetypeId, setInspectingArchetypeId] = useState(null); const [inspectingTemplateId, setInspectingTemplateId] = useState(null); + const [showArchetypeList, setShowArchetypeList] = useState(false); + const [showTemplateList, setShowTemplateList] = useState(false); const [selectedArchetypeForPrep, setSelectedArchetypeForPrep] = useState(''); const [isPrepping, setIsPrepping] = useState(false); const [prepSuccess, setPrepSuccess] = useState(false); const [quickAssignDropdown, setQuickAssignDropdown] = useState(null); + const [needsAgentCollapsed, setNeedsAgentCollapsed] = useState(false); + const [preAssignedCollapsed, setPreAssignedCollapsed] = useState(false); + const [squadRosterCollapsed, setSquadRosterCollapsed] = useState(false); + const { archetypes, saveArchetype, deleteArchetype } = useArchetypes(projectRoot); const { templates, saveTemplate, deleteTemplate } = useTemplates(projectRoot); const { actionableNodeIds } = useGraphAnalysis(issues, projectRoot, null); + const selectedEpic = useMemo(() => { + if (!epicId) return null; + return issues.find(issue => issue.id === epicId && issue.issue_type === 'epic') || null; + }, [issues, epicId]); + + const epicTemplate = useMemo(() => { + const templateId = selectedEpic ? getTemplateId(selectedEpic) : null; + if (!templateId) return null; + return templates.find(t => t.id === templateId) || null; + }, [templates, selectedEpic]); + const needsAgentTasks = useMemo(() => { return issues.filter(issue => { if (issue.status === 'closed') return false; if (!actionableNodeIds.has(issue.id)) return false; if (hasAgentLabel(issue.labels)) return false; - // Filter by selected epic if (epicId) { const hasParentEpic = issue.dependencies.some( dep => dep.type === 'parent' && dep.target === epicId @@ -67,7 +93,6 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }: if (issue.status === 'in_progress') return false; if (issue.status === 'closed') return false; if (!hasAgentLabel(issue.labels)) return false; - // Filter by selected epic if (epicId) { const hasParentEpic = issue.dependencies.some( dep => dep.type === 'parent' && dep.target === epicId @@ -100,6 +125,50 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }: }); }, [issues, archetypes, epicId]); + const handleApplyTemplateToEpic = async (templateId: string, targetEpicId: string) => { + try { + const res = await fetch('/api/beads/update', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + projectRoot, + id: targetEpicId, + metadata: { templateId } + }) + }); + + if (!res.ok) { + throw new Error('Failed to apply template'); + } + + console.log('Template applied successfully:', { templateId, epicId: targetEpicId }); + } catch (error) { + console.error('Failed to apply template:', error); + } + }; + + const handleRemoveTemplateFromEpic = async (targetEpicId: string) => { + try { + const res = await fetch('/api/beads/update', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + projectRoot, + id: targetEpicId, + metadata: { templateId: null } + }) + }); + + if (!res.ok) { + throw new Error('Failed to remove template'); + } + + console.log('Template removed successfully'); + } catch (error) { + console.error('Failed to remove template:', error); + } + }; + const handlePrepTask = async () => { if (!selectedIssue || !selectedArchetypeForPrep) return; @@ -158,6 +227,22 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }: ); }; + const cloneTemplate = async (template: SwarmTemplate) => { + await saveTemplate({ + name: `${template.name} (Copy)`, + description: template.description, + team: template.team, + protoFormula: template.protoFormula, + color: template.color, + icon: template.icon, + isBuiltIn: false + }); + }; + + const getArchetypeCountInTeam = (template: SwarmTemplate, archetypeId: string): number => { + return template.team.filter(member => member.archetypeId === archetypeId).length; + }; + const renderTaskItem = (issue: BeadIssue, showAssignButton: boolean = false, archetypeBadges: AgentArchetype[] = []) => (
@@ -210,22 +295,152 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }:
- {selectedIssue && ( + setShowArchetypeList(false)} + onSelect={(archetype) => { + setSelectedArchetypeForPrep(archetype.id); + setShowArchetypeList(false); + }} + onEdit={(archetypeId) => { + setInspectingArchetypeId(archetypeId); + setShowArchetypeList(false); + }} + onCreateNew={() => { + setInspectingArchetypeId(''); + setShowArchetypeList(false); + }} + /> + + setShowTemplateList(false)} + onSelect={(template) => { + if (selectedEpic) { + handleApplyTemplateToEpic(template.id, selectedEpic.id); + } + setShowTemplateList(false); + }} + onEdit={(templateId) => { + setInspectingTemplateId(templateId); + setShowTemplateList(false); + }} + onCreateNew={() => { + setInspectingTemplateId(''); + setShowTemplateList(false); + }} + /> + + {selectedEpic && ( +
+
+ +

Epic Template

+
+
+
+
{selectedEpic.id}
+
{selectedEpic.title}
+
+ + {epicTemplate ? ( +
+
+
+
+ {epicTemplate.icon || 'T'} +
+
{epicTemplate.name}
+
+ {epicTemplate.description && ( +
{epicTemplate.description}
+ )} +
+
Team Roster
+
+ {Array.from(new Set(epicTemplate.team.map(m => m.archetypeId))).map(archetypeId => { + const archetype = archetypes.find((a: AgentArchetype) => a.id === archetypeId); + const count = getArchetypeCountInTeam(epicTemplate, archetypeId); + if (!archetype) return null; + return ( +
+
+ {getArchetypeDisplayChar(archetype)} +
+ {archetype.name} + x{count} +
+ ); + })} +
+
+
+
+ + +
+
+ ) : ( +
+
+ No template assigned +
+ +
+ )} +
+
+ )} + + {selectedIssue && selectedIssue.issue_type !== 'epic' && (
@@ -271,76 +486,94 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }: )}
-
+
-
- {needsAgentTasks.length === 0 ? ( -
- All actionable tasks have agents assigned -
- ) : ( - needsAgentTasks.map(issue => renderTaskItem(issue, true)) - )} -
+ + {!needsAgentCollapsed && ( +
+ {needsAgentTasks.length === 0 ? ( +
+ All actionable tasks have agents assigned +
+ ) : ( + needsAgentTasks.map(issue => renderTaskItem(issue, true)) + )} +
+ )}
-
+
-
- {preAssignedTasks.length === 0 ? ( -
- No pre-assigned tasks waiting -
- ) : ( - preAssignedTasks.map(issue => { - const agentLabels = getAgentLabels(issue.labels); - const archetypeBadges = agentLabels - .map(label => getArchetypeForAgentLabel(label)) - .filter((a): a is AgentArchetype => a !== undefined); - return renderTaskItem(issue, false, archetypeBadges); - }) - )} -
+ + {!preAssignedCollapsed && ( +
+ {preAssignedTasks.length === 0 ? ( +
+ No pre-assigned tasks waiting +
+ ) : ( + preAssignedTasks.map(issue => { + const agentLabels = getAgentLabels(issue.labels); + const archetypeBadges = agentLabels + .map(label => getArchetypeForAgentLabel(label)) + .filter((a): a is AgentArchetype => a !== undefined); + return renderTaskItem(issue, false, archetypeBadges); + }) + )} +
+ )}
-
+
-
- {activeRoster.length === 0 ? ( -
- No active agents -
- ) : ( - activeRoster.map(({ issue, archetype }) => ( -
-
- {archetype?.name.charAt(0) || '?'} -
-
-
{issue.assignee}
-
{issue.id}
-
+ + {!squadRosterCollapsed && ( +
+ {activeRoster.length === 0 ? ( +
+ No active agents
- )) - )} -
+ ) : ( + activeRoster.map(({ issue, archetype }) => ( +
+
+ {archetype ? getArchetypeDisplayChar(archetype) : '?'} +
+
+
{issue.assignee}
+
{issue.id}
+
+
+ )) + )} +
+ )}
{inspectingArchetypeId !== null && ( @@ -359,6 +592,7 @@ export function AssignmentPanel({ selectedIssue, projectRoot, issues, epicId }: onClose={() => setInspectingTemplateId(null)} onSave={saveTemplate} onDelete={deleteTemplate} + onClone={cloneTemplate} /> )}