feat(team): implement multi-agent team collaboration UI

Phase 6 progress - Multi-Agent Team Collaboration:

Types (types/team.ts):
- Team, TeamMember, TeamTask type definitions
- Dev↔QA Loop state machine types
- CollaborationEvent and TeamMetrics types

Store (store/teamStore.ts):
- Team CRUD operations with localStorage persistence
- Task assignment and status management
- Dev↔QA loop lifecycle management
- Real-time collaboration events

Components:
- TeamOrchestrator.tsx: Team creation, member/task management UI
- DevQALoop.tsx: Developer↔QA review loop visualization
- TeamCollaborationView.tsx: Real-time collaboration dashboard

Features:
- 6 agent roles: orchestrator, developer, reviewer, tester, architect, specialist
- 7 task statuses: pending → assigned → in_progress → review → completed/failed
- Dev↔QA loop with max 3 iterations before escalation
- 4 collaboration patterns: sequential, parallel, pipeline, review_loop
- Live event feed with auto-scroll
- Team metrics: completion rate, pass rate, efficiency score

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-15 03:22:59 +08:00
parent acdbf20848
commit c91740c00e
7 changed files with 2292 additions and 2 deletions

View File

@@ -0,0 +1,587 @@
/**
* Team Store - Multi-Agent Team Collaboration State Management
*
* Manages team orchestration, task assignment, Dev↔QA loops,
* and real-time collaboration state.
*
* @module store/teamStore
*/
import { create } from 'zustand';
import type {
Team,
TeamMember,
TeamTask,
TeamTaskStatus,
TeamMemberRole,
DevQALoop,
DevQALoopState,
CollaborationPattern,
CreateTeamRequest,
AddTeamTaskRequest,
TeamMetrics,
CollaborationEvent,
ReviewFeedback,
TaskDeliverable,
} from '../types/team';
import { getGatewayClient } from '../lib/gateway-client';
// === Store State ===
interface TeamStoreState {
// Data
teams: Team[];
activeTeam: Team | null;
metrics: TeamMetrics | null;
// UI State
isLoading: boolean;
error: string | null;
selectedTaskId: string | null;
selectedMemberId: string | null;
// Real-time events
recentEvents: CollaborationEvent[];
// Actions - Team Management
loadTeams: () => Promise<void>;
createTeam: (request: CreateTeamRequest) => Promise<Team | null>;
deleteTeam: (teamId: string) => Promise<boolean>;
setActiveTeam: (team: Team | null) => void;
// Actions - Member Management
addMember: (teamId: string, agentId: string, role: TeamMemberRole) => Promise<TeamMember | null>;
removeMember: (teamId: string, memberId: string) => Promise<boolean>;
updateMemberRole: (teamId: string, memberId: string, role: TeamMemberRole) => Promise<boolean>;
// Actions - Task Management
addTask: (request: AddTeamTaskRequest) => Promise<TeamTask | null>;
updateTaskStatus: (teamId: string, taskId: string, status: TeamTaskStatus) => Promise<boolean>;
assignTask: (teamId: string, taskId: string, memberId: string) => Promise<boolean>;
submitDeliverable: (teamId: string, taskId: string, deliverable: TaskDeliverable) => Promise<boolean>;
// Actions - Dev↔QA Loop
startDevQALoop: (teamId: string, taskId: string, developerId: string, reviewerId: string) => Promise<DevQALoop | null>;
submitReview: (teamId: string, loopId: string, feedback: Omit<ReviewFeedback, 'reviewedAt' | 'reviewerId'>) => Promise<boolean>;
updateLoopState: (teamId: string, loopId: string, state: DevQALoopState) => Promise<boolean>;
// Actions - Events
addEvent: (event: CollaborationEvent) => void;
clearEvents: () => void;
// Actions - UI
setSelectedTask: (taskId: string | null) => void;
setSelectedMember: (memberId: string | null) => void;
clearError: () => void;
}
// === Helper Functions ===
const generateId = () => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const calculateMetrics = (team: Team): TeamMetrics => {
const completedTasks = team.tasks.filter(t => t.status === 'completed');
const totalTasks = team.tasks.length;
const reviewedTasks = completedTasks.filter(t => t.reviewFeedback);
const avgCompletionTime = completedTasks.length > 0
? completedTasks.reduce((sum, t) => {
if (t.startedAt && t.completedAt) {
return sum + (new Date(t.completedAt).getTime() - new Date(t.startedAt).getTime());
}
return sum;
}, 0) / completedTasks.length
: 0;
const approvedReviews = reviewedTasks.filter(t => t.reviewFeedback?.verdict === 'approved');
const passRate = reviewedTasks.length > 0
? (approvedReviews.length / reviewedTasks.length) * 100
: 0;
const totalIterations = team.activeLoops.reduce((sum, loop) => sum + loop.iterationCount, 0);
const avgIterations = team.activeLoops.length > 0
? totalIterations / team.activeLoops.length
: 0;
const escalations = team.activeLoops.filter(loop => loop.state === 'escalated').length;
const efficiency = totalTasks > 0
? Math.min(100, (completedTasks.length / totalTasks) * 100 * (passRate / 100))
: 0;
return {
tasksCompleted: completedTasks.length,
avgCompletionTime,
passRate,
avgIterations,
escalations,
efficiency,
};
};
// === Store Implementation ===
export const useTeamStore = create<TeamStoreState>((set, get) => ({
// Initial State
teams: [],
activeTeam: null,
metrics: null,
isLoading: false,
error: null,
selectedTaskId: null,
selectedMemberId: null,
recentEvents: [],
// Team Management
loadTeams: async () => {
set({ isLoading: true, error: null });
try {
const client = getGatewayClient();
// For now, load from localStorage until API is available
const stored = localStorage.getItem('zclaw-teams');
const teams: Team[] = stored ? JSON.parse(stored) : [];
set({ teams, isLoading: false });
} catch (error) {
set({ error: (error as Error).message, isLoading: false });
}
},
createTeam: async (request: CreateTeamRequest) => {
set({ isLoading: true, error: null });
try {
const now = new Date().toISOString();
const members: TeamMember[] = request.memberAgents.map((m, index) => ({
id: generateId(),
agentId: m.agentId,
name: `Agent-${m.agentId.slice(0, 4)}`,
role: m.role,
skills: [],
workload: 0,
status: 'idle',
maxConcurrentTasks: m.role === 'orchestrator' ? 5 : 2,
currentTasks: [],
}));
const team: Team = {
id: generateId(),
name: request.name,
description: request.description,
members,
tasks: [],
pattern: request.pattern,
activeLoops: [],
status: 'active',
createdAt: now,
updatedAt: now,
};
set(state => {
const teams = [...state.teams, team];
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return { teams, activeTeam: team, isLoading: false };
});
return team;
} catch (error) {
set({ error: (error as Error).message, isLoading: false });
return null;
}
},
deleteTeam: async (teamId: string) => {
set({ isLoading: true, error: null });
try {
set(state => {
const teams = state.teams.filter(t => t.id !== teamId);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === teamId ? null : state.activeTeam,
isLoading: false
};
});
return true;
} catch (error) {
set({ error: (error as Error).message, isLoading: false });
return false;
}
},
setActiveTeam: (team: Team | null) => {
set(state => ({
activeTeam: team,
metrics: team ? calculateMetrics(team) : null,
}));
},
// Member Management
addMember: async (teamId: string, agentId: string, role: TeamMemberRole) => {
const state = get();
const team = state.teams.find(t => t.id === teamId);
if (!team) return null;
const member: TeamMember = {
id: generateId(),
agentId,
name: `Agent-${agentId.slice(0, 4)}`,
role,
skills: [],
workload: 0,
status: 'idle',
maxConcurrentTasks: role === 'orchestrator' ? 5 : 2,
currentTasks: [],
};
const updatedTeam = {
...team,
members: [...team.members, member],
updatedAt: new Date().toISOString(),
};
set(state => {
const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam,
};
});
return member;
},
removeMember: async (teamId: string, memberId: string) => {
const state = get();
const team = state.teams.find(t => t.id === teamId);
if (!team) return false;
const updatedTeam = {
...team,
members: team.members.filter(m => m.id !== memberId),
updatedAt: new Date().toISOString(),
};
set(state => {
const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam,
};
});
return true;
},
updateMemberRole: async (teamId: string, memberId: string, role: TeamMemberRole) => {
const state = get();
const team = state.teams.find(t => t.id === teamId);
if (!team) return false;
const updatedTeam = {
...team,
members: team.members.map(m =>
m.id === memberId ? { ...m, role, maxConcurrentTasks: role === 'orchestrator' ? 5 : 2 } : m
),
updatedAt: new Date().toISOString(),
};
set(state => {
const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam,
};
});
return true;
},
// Task Management
addTask: async (request: AddTeamTaskRequest) => {
const state = get();
const team = state.teams.find(t => t.id === request.teamId);
if (!team) return null;
const now = new Date().toISOString();
const task: TeamTask = {
id: generateId(),
title: request.title,
description: request.description,
status: request.assigneeId ? 'assigned' : 'pending',
priority: request.priority,
assigneeId: request.assigneeId,
dependencies: request.dependencies || [],
type: request.type,
estimate: request.estimate,
createdAt: now,
updatedAt: now,
};
const updatedTeam = {
...team,
tasks: [...team.tasks, task],
updatedAt: now,
};
set(state => {
const teams = state.teams.map(t => t.id === request.teamId ? updatedTeam : t);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === request.teamId ? updatedTeam : state.activeTeam,
metrics: state.activeTeam?.id === request.teamId ? calculateMetrics(updatedTeam) : state.metrics,
};
});
return task;
},
updateTaskStatus: async (teamId: string, taskId: string, status: TeamTaskStatus) => {
const state = get();
const team = state.teams.find(t => t.id === teamId);
if (!team) return false;
const now = new Date().toISOString();
const updatedTeam = {
...team,
tasks: team.tasks.map(t => {
if (t.id !== taskId) return t;
const updates: Partial<TeamTask> = { status, updatedAt: now };
if (status === 'in_progress' && !t.startedAt) {
updates.startedAt = now;
}
if (status === 'completed') {
updates.completedAt = now;
}
return { ...t, ...updates };
}),
updatedAt: now,
};
set(state => {
const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam,
metrics: state.activeTeam?.id === teamId ? calculateMetrics(updatedTeam) : state.metrics,
};
});
return true;
},
assignTask: async (teamId: string, taskId: string, memberId: string) => {
const state = get();
const team = state.teams.find(t => t.id === teamId);
if (!team) return false;
const now = new Date().toISOString();
const updatedTeam = {
...team,
tasks: team.tasks.map(t =>
t.id === taskId
? { ...t, assigneeId: memberId, status: 'assigned' as TeamTaskStatus, updatedAt: now }
: t
),
members: team.members.map(m =>
m.id === memberId
? { ...m, currentTasks: [...m.currentTasks, taskId], workload: (m.workload + 25) }
: m
),
updatedAt: now,
};
set(state => {
const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam,
};
});
return true;
},
submitDeliverable: async (teamId: string, taskId: string, deliverable: TaskDeliverable) => {
const state = get();
const team = state.teams.find(t => t.id === teamId);
if (!team) return false;
const now = new Date().toISOString();
const updatedTeam = {
...team,
tasks: team.tasks.map(t =>
t.id === taskId
? { ...t, deliverable, status: 'review' as TeamTaskStatus, updatedAt: now }
: t
),
updatedAt: now,
};
set(state => {
const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam,
};
});
return true;
},
// Dev↔QA Loop
startDevQALoop: async (teamId: string, taskId: string, developerId: string, reviewerId: string) => {
const state = get();
const team = state.teams.find(t => t.id === teamId);
if (!team) return null;
const now = new Date().toISOString();
const loop: DevQALoop = {
id: generateId(),
developerId,
reviewerId,
taskId,
state: 'developing',
iterationCount: 0,
maxIterations: 3,
feedbackHistory: [],
startedAt: now,
lastUpdatedAt: now,
};
const updatedTeam = {
...team,
activeLoops: [...team.activeLoops, loop],
updatedAt: now,
};
set(state => {
const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam,
};
});
return loop;
},
submitReview: async (teamId: string, loopId: string, feedback: Omit<ReviewFeedback, 'reviewedAt' | 'reviewerId'>) => {
const state = get();
const team = state.teams.find(t => t.id === teamId);
if (!team) return false;
const loop = team.activeLoops.find(l => l.id === loopId);
if (!loop) return false;
const now = new Date().toISOString();
const fullFeedback: ReviewFeedback = {
...feedback,
reviewedAt: now,
reviewerId: loop.reviewerId,
};
let newState: DevQALoopState;
let newIterationCount = loop.iterationCount;
if (feedback.verdict === 'approved') {
newState = 'approved';
} else if (newIterationCount >= loop.maxIterations - 1) {
newState = 'escalated';
} else {
newState = 'revising';
newIterationCount++;
}
const updatedTeam = {
...team,
tasks: team.tasks.map(t =>
t.id === loop.taskId
? { ...t, reviewFeedback: fullFeedback, updatedAt: now }
: t
),
activeLoops: team.activeLoops.map(l =>
l.id === loopId
? {
...l,
state: newState,
iterationCount: newIterationCount,
feedbackHistory: [...l.feedbackHistory, fullFeedback],
lastUpdatedAt: now,
}
: l
),
updatedAt: now,
};
set(state => {
const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam,
metrics: state.activeTeam?.id === teamId ? calculateMetrics(updatedTeam) : state.metrics,
};
});
return true;
},
updateLoopState: async (teamId: string, loopId: string, state: DevQALoopState) => {
const teamStore = get();
const team = teamStore.teams.find(t => t.id === teamId);
if (!team) return false;
const now = new Date().toISOString();
const updatedTeam = {
...team,
activeLoops: team.activeLoops.map(l =>
l.id === loopId
? { ...l, state, lastUpdatedAt: now }
: l
),
updatedAt: now,
};
set(state => {
const teams = state.teams.map(t => t.id === teamId ? updatedTeam : t);
localStorage.setItem('zclaw-teams', JSON.stringify(teams));
return {
teams,
activeTeam: state.activeTeam?.id === teamId ? updatedTeam : state.activeTeam,
};
});
return true;
},
// Events
addEvent: (event: CollaborationEvent) => {
set(state => ({
recentEvents: [event, ...state.recentEvents].slice(0, 100),
}));
},
clearEvents: () => {
set({ recentEvents: [] });
},
// UI
setSelectedTask: (taskId: string | null) => {
set({ selectedTaskId: taskId });
},
setSelectedMember: (memberId: string | null) => {
set({ selectedMemberId: memberId });
},
clearError: () => {
set({ error: null });
},
}));