Refactor team collaboration skills and update documentation

- Renamed `team-lifecycle-v5` to `team-lifecycle` across various documentation files for consistency.
- Updated references in code examples and usage sections to reflect the new skill name.
- Added a new command file for the `monitor` functionality in the `team-iterdev` skill, detailing the coordinator's monitoring events and task management.
- Introduced new components for dynamic pipeline visualization and session coordinates display in the frontend.
- Implemented utility functions for pipeline stage detection and status derivation based on message history.
- Enhanced the team role panel to map members to their respective pipeline roles with status indicators.
- Updated Chinese documentation to reflect the changes in skill names and descriptions.
This commit is contained in:
catlog22
2026-03-04 11:07:48 +08:00
parent 5e96722c09
commit ffd5282932
132 changed files with 2938 additions and 18916 deletions

View File

@@ -0,0 +1,139 @@
// ========================================
// DynamicPipeline Component
// ========================================
// Dynamic pipeline stage visualization with fallback to static TeamPipeline
import { useIntl } from 'react-intl';
import { CheckCircle2, Circle, Loader2, Ban, SkipForward } from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/lib/utils';
import { TeamPipeline } from './TeamPipeline';
import type { DynamicStage, DynamicStageStatus, PhaseInfo } from '@/types/team';
interface DynamicPipelineProps {
stages: DynamicStage[];
phaseInfo?: PhaseInfo | null;
}
const statusConfig: Record<DynamicStageStatus, { icon: typeof CheckCircle2; color: string; bg: string; animate?: boolean }> = {
completed: { icon: CheckCircle2, color: 'text-green-500', bg: 'bg-green-500/10 border-green-500/30' },
in_progress: { icon: Loader2, color: 'text-blue-500', bg: 'bg-blue-500/10 border-blue-500/30', animate: true },
pending: { icon: Circle, color: 'text-muted-foreground', bg: 'bg-muted border-border' },
blocked: { icon: Ban, color: 'text-red-500', bg: 'bg-red-500/10 border-red-500/30' },
skipped: { icon: SkipForward, color: 'text-yellow-500', bg: 'bg-yellow-500/10 border-yellow-500/30' },
};
const LEGEND_STATUSES: DynamicStageStatus[] = ['completed', 'in_progress', 'pending', 'blocked', 'skipped'];
function StageNode({ stage }: { stage: DynamicStage }) {
const { formatMessage } = useIntl();
const config = statusConfig[stage.status];
const Icon = config.icon;
const statusKey = stage.status === 'in_progress' ? 'inProgress' : stage.status;
return (
<div
className={cn(
'flex flex-col items-center gap-1.5 px-4 py-3 rounded-lg border-2 min-w-[90px] transition-all',
config.bg
)}
>
<Icon
className={cn('w-5 h-5', config.color, config.animate && 'animate-spin')}
style={config.animate ? { animationDuration: '2s' } : undefined}
/>
<span className="text-xs font-medium">
{stage.label}
</span>
<span className={cn('text-[10px]', config.color)}>
{formatMessage({ id: `team.pipeline.${statusKey}` })}
</span>
{stage.role && (
<span className="text-[10px] text-muted-foreground truncate max-w-[80px]">
{stage.role}
</span>
)}
</div>
);
}
function Arrow() {
return (
<div className="flex items-center px-1">
<div className="w-6 h-0.5 bg-border" />
<div className="w-0 h-0 border-t-[4px] border-t-transparent border-b-[4px] border-b-transparent border-l-[6px] border-l-border" />
</div>
);
}
export function DynamicPipeline({ stages, phaseInfo }: DynamicPipelineProps) {
const { formatMessage } = useIntl();
// Fallback to static pipeline when no dynamic stages
if (stages.length === 0) {
return <TeamPipeline messages={[]} />;
}
return (
<div className="flex flex-col h-full">
<div className="space-y-3 flex-1">
<h3 className="text-sm font-medium text-muted-foreground">
{formatMessage({ id: 'team.pipeline.title' })}
</h3>
{/* Phase header */}
{phaseInfo && (
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="secondary">
{formatMessage({ id: 'team.coordinates.phase' })}: {phaseInfo.currentPhase}
{phaseInfo.totalPhases != null && `/${phaseInfo.totalPhases}`}
</Badge>
{phaseInfo.currentStep && (
<Badge variant="secondary">
{phaseInfo.currentStep}
</Badge>
)}
{phaseInfo.gapIteration > 0 && (
<Badge variant="outline">
{formatMessage({ id: 'team.coordinates.gap' })}: {phaseInfo.gapIteration}
</Badge>
)}
</div>
)}
{/* Desktop: horizontal layout */}
<div className="hidden sm:flex items-center gap-0">
{stages.map((stage, idx) => (
<div key={stage.id} className="flex items-center">
{idx > 0 && <Arrow />}
<StageNode stage={stage} />
</div>
))}
</div>
{/* Mobile: vertical layout */}
<div className="flex sm:hidden flex-col items-center gap-2">
{stages.map((stage) => (
<StageNode key={stage.id} stage={stage} />
))}
</div>
</div>
{/* Legend */}
<div className="flex flex-wrap gap-3 text-[10px] text-muted-foreground mt-auto pt-3 border-t border-border">
{LEGEND_STATUSES.map((s) => {
const cfg = statusConfig[s];
const Icon = cfg.icon;
const statusKey = s === 'in_progress' ? 'inProgress' : s;
return (
<span key={s} className="flex items-center gap-1">
<Icon className={cn('w-3 h-3', cfg.color)} />
{formatMessage({ id: `team.pipeline.${statusKey}` })}
</span>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
// ========================================
// SessionCoordinates Component
// ========================================
// Compact inline display of phase/step/gap using Badge components
import { useIntl } from 'react-intl';
import { Badge } from '@/components/ui/Badge';
import type { PhaseInfo } from '@/types/team';
interface SessionCoordinatesProps {
phaseInfo: PhaseInfo;
}
export function SessionCoordinates({ phaseInfo }: SessionCoordinatesProps) {
const { formatMessage } = useIntl();
return (
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="secondary">
{formatMessage({ id: 'team.coordinates.phase' })}: {phaseInfo.currentPhase}
{phaseInfo.totalPhases != null && `/${phaseInfo.totalPhases}`}
</Badge>
{phaseInfo.currentStep && (
<Badge variant="secondary">
{formatMessage({ id: 'team.coordinates.step' })}: {phaseInfo.currentStep}
</Badge>
)}
{phaseInfo.gapIteration > 0 && (
<Badge variant="outline">
{formatMessage({ id: 'team.coordinates.gap' })}: {phaseInfo.gapIteration}
</Badge>
)}
</div>
);
}

View File

@@ -4,7 +4,7 @@
// Detail view header with back button, stats, and controls
import { useIntl } from 'react-intl';
import { Users, MessageSquare, RefreshCw, ArrowLeft } from 'lucide-react';
import { Users, MessageSquare, RefreshCw, ArrowLeft, GitBranch } from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { Switch } from '@/components/ui/Switch';
@@ -18,6 +18,8 @@ interface TeamHeaderProps {
totalMessages: number;
autoRefresh: boolean;
onToggleAutoRefresh: () => void;
skillType?: string;
pipelineMode?: string;
}
export function TeamHeader({
@@ -27,6 +29,8 @@ export function TeamHeader({
totalMessages,
autoRefresh,
onToggleAutoRefresh,
skillType,
pipelineMode,
}: TeamHeaderProps) {
const { formatMessage } = useIntl();
@@ -55,6 +59,23 @@ export function TeamHeader({
{formatMessage({ id: 'team.messages' })}: {totalMessages}
</Badge>
</div>
{/* Skill type and pipeline mode badges */}
{(skillType || pipelineMode) && (
<div className="flex items-center gap-2">
{skillType && (
<Badge variant="outline" className="gap-1">
{skillType}
</Badge>
)}
{pipelineMode && (
<Badge variant="default" className="gap-1">
<GitBranch className="w-3 h-3" />
{pipelineMode}
</Badge>
)}
</div>
)}
</>
)}
</div>

View File

@@ -0,0 +1,148 @@
// ========================================
// TeamRolePanel Component
// ========================================
// Member-to-pipeline-role mapping with fallback to TeamMembersPanel
import { useIntl } from 'react-intl';
import { Card, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/lib/utils';
import { TeamMembersPanel } from './TeamMembersPanel';
import type { TeamMember, DynamicStage } from '@/types/team';
interface TeamRolePanelProps {
members: TeamMember[];
stages: DynamicStage[];
roleState?: Record<string, Record<string, unknown>>;
}
function formatRelativeTime(isoString: string): string {
if (!isoString) return '';
const now = Date.now();
const then = new Date(isoString).getTime();
const diffMs = now - then;
if (diffMs < 0) return 'now';
const seconds = Math.floor(diffMs / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
const days = Math.floor(hours / 24);
return `${days}d`;
}
function getMemberStatus(member: TeamMember): 'active' | 'idle' {
if (!member.lastSeen) return 'idle';
const diffMs = Date.now() - new Date(member.lastSeen).getTime();
// Active if seen in last 2 minutes
return diffMs < 2 * 60 * 1000 ? 'active' : 'idle';
}
/** Find the matching dynamic stage for a member */
function findMatchingStage(member: TeamMember, stages: DynamicStage[]): DynamicStage | undefined {
const memberLower = member.member.toLowerCase();
return stages.find(
(stage) =>
(stage.role && stage.role.toLowerCase() === memberLower) ||
stage.id.toLowerCase() === memberLower
);
}
const stageBadgeVariant: Record<string, 'success' | 'info' | 'secondary' | 'destructive' | 'warning'> = {
completed: 'success',
in_progress: 'info',
pending: 'secondary',
blocked: 'destructive',
skipped: 'warning',
};
export function TeamRolePanel({ members, stages, roleState: _roleState }: TeamRolePanelProps) {
const { formatMessage } = useIntl();
// Fallback to static members panel when no dynamic stages
if (stages.length === 0) {
return <TeamMembersPanel members={members} />;
}
return (
<div className="space-y-3">
<h3 className="text-sm font-medium text-muted-foreground">
{formatMessage({ id: 'team.membersPanel.title' })}
</h3>
<div className="space-y-2">
{members.map((m) => {
const status = getMemberStatus(m);
const isActive = status === 'active';
const matchedStage = findMatchingStage(m, stages);
return (
<Card key={m.member} className="overflow-hidden">
<CardContent className="p-3">
<div className="flex items-start gap-3">
{/* Status indicator */}
<div className="pt-0.5">
<div
className={cn(
'w-2.5 h-2.5 rounded-full',
isActive
? 'bg-green-500 shadow-[0_0_6px_rgba(34,197,94,0.5)]'
: 'bg-muted-foreground/40'
)}
/>
</div>
<div className="flex-1 min-w-0 space-y-1">
{/* Name + status badge + stage badge */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium truncate">{m.member}</span>
<Badge
variant={isActive ? 'success' : 'secondary'}
className="text-[10px] px-1.5 py-0"
>
{formatMessage({ id: `team.membersPanel.${status}` })}
</Badge>
{matchedStage && (
<Badge
variant={stageBadgeVariant[matchedStage.status] ?? 'secondary'}
className="text-[10px] px-1.5 py-0"
>
{matchedStage.label}
</Badge>
)}
</div>
{/* Last action */}
{m.lastAction && (
<p className="text-xs text-muted-foreground truncate">
{m.lastAction}
</p>
)}
{/* Stats row */}
<div className="flex items-center gap-3 text-[10px] text-muted-foreground">
<span>
{m.messageCount} {formatMessage({ id: 'team.messages' }).toLowerCase()}
</span>
{m.lastSeen && (
<span>
{formatRelativeTime(m.lastSeen)} {formatMessage({ id: 'team.membersPanel.ago' })}
</span>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
})}
{members.length === 0 && (
<p className="text-xs text-muted-foreground text-center py-4">
{formatMessage({ id: 'team.empty.noMessages' })}
</p>
)}
</div>
</div>
);
}