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:
587
desktop/src/store/teamStore.ts
Normal file
587
desktop/src/store/teamStore.ts
Normal 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 });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user