mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge remote-tracking branch 'upstream/feature/v0.14.0rc-1768981697539-gg62' into feature/unified-sidebar
# Conflicts: # apps/server/src/index.ts # apps/ui/src/components/layout/sidebar/components/sidebar-footer.tsx # libs/types/src/index.ts
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
Trash2,
|
||||
Search,
|
||||
X,
|
||||
LayoutDashboard,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
@@ -556,9 +557,31 @@ export function DashboardView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Projects Overview button */}
|
||||
{hasProjects && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate({ to: '/overview' })}
|
||||
className="hidden sm:flex gap-2 titlebar-no-drag"
|
||||
data-testid="projects-overview-button"
|
||||
>
|
||||
<LayoutDashboard className="w-4 h-4" />
|
||||
Overview
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Mobile action buttons in header */}
|
||||
{hasProjects && (
|
||||
<div className="flex sm:hidden gap-2 titlebar-no-drag">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => navigate({ to: '/overview' })}
|
||||
title="Projects Overview"
|
||||
>
|
||||
<LayoutDashboard className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onClick={handleOpenProject}>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
283
apps/ui/src/components/views/overview-view.tsx
Normal file
283
apps/ui/src/components/views/overview-view.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* OverviewView - Multi-project dashboard showing status across all projects
|
||||
*
|
||||
* Provides a unified view of all projects with active features, running agents,
|
||||
* recent completions, and alerts. Quick navigation to any project or feature.
|
||||
*/
|
||||
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useMultiProjectStatus } from '@/hooks/use-multi-project-status';
|
||||
import { isElectron } from '@/lib/electron';
|
||||
import { isMac } from '@/lib/utils';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ProjectStatusCard } from './overview/project-status-card';
|
||||
import { RecentActivityFeed } from './overview/recent-activity-feed';
|
||||
import { RunningAgentsPanel } from './overview/running-agents-panel';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
RefreshCw,
|
||||
Folder,
|
||||
Activity,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Clock,
|
||||
Bot,
|
||||
Bell,
|
||||
ArrowLeft,
|
||||
} from 'lucide-react';
|
||||
|
||||
export function OverviewView() {
|
||||
const navigate = useNavigate();
|
||||
const { overview, isLoading, error, refresh } = useMultiProjectStatus(15000); // Refresh every 15s
|
||||
|
||||
const handleBackToDashboard = () => {
|
||||
navigate({ to: '/dashboard' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-screen content-bg" data-testid="overview-view">
|
||||
{/* Header */}
|
||||
<header className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
|
||||
{/* Electron titlebar drag region */}
|
||||
{isElectron() && (
|
||||
<div
|
||||
className={`absolute top-0 left-0 right-0 h-6 titlebar-drag-region z-40 pointer-events-none ${isMac ? 'pl-20' : ''}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<div className="px-4 sm:px-8 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 titlebar-no-drag">
|
||||
<Button variant="ghost" size="icon" onClick={handleBackToDashboard} className="h-8 w-8">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-brand-500/10 flex items-center justify-center">
|
||||
<LayoutDashboard className="w-4 h-4 text-brand-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-foreground">Projects Overview</h1>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{overview ? `${overview.aggregate.projectCounts.total} projects` : 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 titlebar-no-drag">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={refresh}
|
||||
disabled={isLoading}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 sm:p-6">
|
||||
{/* Loading state */}
|
||||
{isLoading && !overview && (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Spinner size="lg" />
|
||||
<p className="text-sm text-muted-foreground">Loading project overview...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{error && !overview && (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-red-500/10 flex items-center justify-center">
|
||||
<XCircle className="w-6 h-6 text-red-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground mb-1">Failed to load overview</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">{error}</p>
|
||||
<Button variant="outline" size="sm" onClick={refresh}>
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{overview && (
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* Aggregate stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
<Card className="bg-card/60">
|
||||
<CardContent className="p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-muted flex items-center justify-center">
|
||||
<Folder className="w-5 h-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{overview.aggregate.projectCounts.total}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Projects</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card/60">
|
||||
<CardContent className="p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center">
|
||||
<Activity className="w-5 h-5 text-green-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{overview.aggregate.featureCounts.running}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Running</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card/60">
|
||||
<CardContent className="p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-yellow-500/10 flex items-center justify-center">
|
||||
<Clock className="w-5 h-5 text-yellow-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{overview.aggregate.featureCounts.pending}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Pending</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card/60">
|
||||
<CardContent className="p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{overview.aggregate.featureCounts.completed}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Completed</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card/60">
|
||||
<CardContent className="p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-red-500/10 flex items-center justify-center">
|
||||
<XCircle className="w-5 h-5 text-red-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{overview.aggregate.featureCounts.failed}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Failed</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card/60">
|
||||
<CardContent className="p-4 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-brand-500/10 flex items-center justify-center">
|
||||
<Bot className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{overview.aggregate.projectsWithAutoModeRunning}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Auto-mode</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Main content grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left column: Project cards */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-foreground">All Projects</h2>
|
||||
{overview.aggregate.totalUnreadNotifications > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Bell className="w-4 h-4" />
|
||||
{overview.aggregate.totalUnreadNotifications} unread notifications
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{overview.projects.length === 0 ? (
|
||||
<Card className="bg-card/60">
|
||||
<CardContent className="py-12 text-center">
|
||||
<Folder className="w-12 h-12 mx-auto mb-4 text-muted-foreground opacity-50" />
|
||||
<h3 className="font-medium text-foreground mb-1">No projects yet</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Create or open a project to get started
|
||||
</p>
|
||||
<Button variant="outline" onClick={handleBackToDashboard}>
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{overview.projects.map((project) => (
|
||||
<ProjectStatusCard key={project.projectId} project={project} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right column: Running agents and activity */}
|
||||
<div className="space-y-4">
|
||||
{/* Running agents */}
|
||||
<Card className="bg-card/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Bot className="w-4 h-4 text-green-500" />
|
||||
Running Agents
|
||||
{overview.aggregate.projectsWithAutoModeRunning > 0 && (
|
||||
<span className="text-xs font-normal text-muted-foreground ml-auto">
|
||||
{overview.aggregate.projectsWithAutoModeRunning} active
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<RunningAgentsPanel projects={overview.projects} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent activity */}
|
||||
<Card className="bg-card/60">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-brand-500" />
|
||||
Recent Activity
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<RecentActivityFeed activities={overview.recentActivity} maxItems={8} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer timestamp */}
|
||||
<div className="text-center text-xs text-muted-foreground pt-4">
|
||||
Last updated: {new Date(overview.generatedAt).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
196
apps/ui/src/components/views/overview/project-status-card.tsx
Normal file
196
apps/ui/src/components/views/overview/project-status-card.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* ProjectStatusCard - Individual project card for multi-project dashboard
|
||||
*
|
||||
* Displays project health, feature counts, and agent status with quick navigation.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { initializeProject } from '@/lib/project-init';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ProjectStatus, ProjectHealthStatus } from '@automaker/types';
|
||||
import { Folder, Activity, CheckCircle2, XCircle, Clock, Pause, Bot, Bell } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface ProjectStatusCardProps {
|
||||
project: ProjectStatus;
|
||||
onProjectClick?: (projectId: string) => void;
|
||||
}
|
||||
|
||||
const healthStatusConfig: Record<
|
||||
ProjectHealthStatus,
|
||||
{ icon: typeof Activity; color: string; label: string; bgColor: string }
|
||||
> = {
|
||||
active: {
|
||||
icon: Activity,
|
||||
color: 'text-green-500',
|
||||
label: 'Active',
|
||||
bgColor: 'bg-green-500/10',
|
||||
},
|
||||
idle: {
|
||||
icon: Pause,
|
||||
color: 'text-muted-foreground',
|
||||
label: 'Idle',
|
||||
bgColor: 'bg-muted/50',
|
||||
},
|
||||
waiting: {
|
||||
icon: Clock,
|
||||
color: 'text-yellow-500',
|
||||
label: 'Waiting',
|
||||
bgColor: 'bg-yellow-500/10',
|
||||
},
|
||||
completed: {
|
||||
icon: CheckCircle2,
|
||||
color: 'text-blue-500',
|
||||
label: 'Completed',
|
||||
bgColor: 'bg-blue-500/10',
|
||||
},
|
||||
error: {
|
||||
icon: XCircle,
|
||||
color: 'text-red-500',
|
||||
label: 'Error',
|
||||
bgColor: 'bg-red-500/10',
|
||||
},
|
||||
};
|
||||
|
||||
export function ProjectStatusCard({ project, onProjectClick }: ProjectStatusCardProps) {
|
||||
const navigate = useNavigate();
|
||||
const { upsertAndSetCurrentProject } = useAppStore();
|
||||
|
||||
const statusConfig = healthStatusConfig[project.healthStatus];
|
||||
const StatusIcon = statusConfig.icon;
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
if (onProjectClick) {
|
||||
onProjectClick(project.projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default behavior: navigate to project
|
||||
try {
|
||||
const initResult = await initializeProject(project.projectPath);
|
||||
if (!initResult.success) {
|
||||
toast.error('Failed to open project', {
|
||||
description: initResult.error || 'Unknown error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
upsertAndSetCurrentProject(project.projectPath, project.projectName);
|
||||
navigate({ to: '/board' });
|
||||
} catch (error) {
|
||||
toast.error('Failed to open project', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}, [project, onProjectClick, upsertAndSetCurrentProject, navigate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group relative rounded-xl border bg-card/60 backdrop-blur-sm transition-all duration-300 cursor-pointer hover:-translate-y-0.5',
|
||||
project.healthStatus === 'active' && 'border-green-500/30 hover:border-green-500/50',
|
||||
project.healthStatus === 'error' && 'border-red-500/30 hover:border-red-500/50',
|
||||
project.healthStatus === 'waiting' && 'border-yellow-500/30 hover:border-yellow-500/50',
|
||||
project.healthStatus === 'completed' && 'border-blue-500/30 hover:border-blue-500/50',
|
||||
project.healthStatus === 'idle' && 'border-border hover:border-brand-500/40'
|
||||
)}
|
||||
onClick={handleClick}
|
||||
data-testid={`project-status-card-${project.projectId}`}
|
||||
>
|
||||
<div className="p-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-3 mb-3">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
className={cn(
|
||||
'w-10 h-10 rounded-lg flex items-center justify-center shrink-0 transition-colors',
|
||||
statusConfig.bgColor
|
||||
)}
|
||||
>
|
||||
<Folder className={cn('w-5 h-5', statusConfig.color)} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-medium text-foreground truncate group-hover:text-brand-500 transition-colors">
|
||||
{project.projectName}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground truncate">{project.projectPath}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status badge */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{project.unreadNotificationCount > 0 && (
|
||||
<Badge variant="destructive" className="h-5 px-1.5 text-xs">
|
||||
<Bell className="w-3 h-3 mr-1" />
|
||||
{project.unreadNotificationCount}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'h-6 px-2 text-xs gap-1',
|
||||
statusConfig.color,
|
||||
project.healthStatus === 'active' && 'border-green-500/30 bg-green-500/10',
|
||||
project.healthStatus === 'error' && 'border-red-500/30 bg-red-500/10'
|
||||
)}
|
||||
>
|
||||
<StatusIcon className="w-3 h-3" />
|
||||
{statusConfig.label}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature counts */}
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{project.featureCounts.running > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-md bg-green-500/10 text-green-600 dark:text-green-400">
|
||||
<Activity className="w-3 h-3" />
|
||||
{project.featureCounts.running} running
|
||||
</div>
|
||||
)}
|
||||
{project.featureCounts.pending > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-md bg-yellow-500/10 text-yellow-600 dark:text-yellow-400">
|
||||
<Clock className="w-3 h-3" />
|
||||
{project.featureCounts.pending} pending
|
||||
</div>
|
||||
)}
|
||||
{project.featureCounts.completed > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-md bg-blue-500/10 text-blue-600 dark:text-blue-400">
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
{project.featureCounts.completed} completed
|
||||
</div>
|
||||
)}
|
||||
{project.featureCounts.failed > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-md bg-red-500/10 text-red-600 dark:text-red-400">
|
||||
<XCircle className="w-3 h-3" />
|
||||
{project.featureCounts.failed} failed
|
||||
</div>
|
||||
)}
|
||||
{project.featureCounts.verified > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-md bg-purple-500/10 text-purple-600 dark:text-purple-400">
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
{project.featureCounts.verified} verified
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer: Total features and auto-mode status */}
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground pt-2 border-t border-border/50">
|
||||
<span>{project.totalFeatures} total features</span>
|
||||
{project.isAutoModeRunning && (
|
||||
<div className="flex items-center gap-1.5 text-green-500">
|
||||
<Bot className="w-3.5 h-3.5 animate-pulse" />
|
||||
<span className="font-medium">Auto-mode active</span>
|
||||
</div>
|
||||
)}
|
||||
{project.lastActivityAt && !project.isAutoModeRunning && (
|
||||
<span>Last activity: {new Date(project.lastActivityAt).toLocaleDateString()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
206
apps/ui/src/components/views/overview/recent-activity-feed.tsx
Normal file
206
apps/ui/src/components/views/overview/recent-activity-feed.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* RecentActivityFeed - Timeline of recent activity across all projects
|
||||
*
|
||||
* Shows completed features, failures, and auto-mode events.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { initializeProject } from '@/lib/project-init';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { RecentActivity, ActivityType, ActivitySeverity } from '@automaker/types';
|
||||
import { CheckCircle2, XCircle, Play, Bot, AlertTriangle, Info, Clock } from 'lucide-react';
|
||||
|
||||
interface RecentActivityFeedProps {
|
||||
activities: RecentActivity[];
|
||||
maxItems?: number;
|
||||
}
|
||||
|
||||
const activityTypeConfig: Record<
|
||||
ActivityType,
|
||||
{ icon: typeof CheckCircle2; defaultColor: string; label: string }
|
||||
> = {
|
||||
feature_created: {
|
||||
icon: Info,
|
||||
defaultColor: 'text-blue-500',
|
||||
label: 'Feature created',
|
||||
},
|
||||
feature_completed: {
|
||||
icon: CheckCircle2,
|
||||
defaultColor: 'text-blue-500',
|
||||
label: 'Feature completed',
|
||||
},
|
||||
feature_verified: {
|
||||
icon: CheckCircle2,
|
||||
defaultColor: 'text-purple-500',
|
||||
label: 'Feature verified',
|
||||
},
|
||||
feature_failed: {
|
||||
icon: XCircle,
|
||||
defaultColor: 'text-red-500',
|
||||
label: 'Feature failed',
|
||||
},
|
||||
feature_started: {
|
||||
icon: Play,
|
||||
defaultColor: 'text-green-500',
|
||||
label: 'Feature started',
|
||||
},
|
||||
auto_mode_started: {
|
||||
icon: Bot,
|
||||
defaultColor: 'text-green-500',
|
||||
label: 'Auto-mode started',
|
||||
},
|
||||
auto_mode_stopped: {
|
||||
icon: Bot,
|
||||
defaultColor: 'text-muted-foreground',
|
||||
label: 'Auto-mode stopped',
|
||||
},
|
||||
ideation_session_started: {
|
||||
icon: Play,
|
||||
defaultColor: 'text-brand-500',
|
||||
label: 'Ideation session started',
|
||||
},
|
||||
ideation_session_ended: {
|
||||
icon: Info,
|
||||
defaultColor: 'text-muted-foreground',
|
||||
label: 'Ideation session ended',
|
||||
},
|
||||
idea_created: {
|
||||
icon: Info,
|
||||
defaultColor: 'text-brand-500',
|
||||
label: 'Idea created',
|
||||
},
|
||||
idea_converted: {
|
||||
icon: CheckCircle2,
|
||||
defaultColor: 'text-green-500',
|
||||
label: 'Idea converted to feature',
|
||||
},
|
||||
notification_created: {
|
||||
icon: AlertTriangle,
|
||||
defaultColor: 'text-yellow-500',
|
||||
label: 'Notification',
|
||||
},
|
||||
project_opened: {
|
||||
icon: Info,
|
||||
defaultColor: 'text-blue-500',
|
||||
label: 'Project opened',
|
||||
},
|
||||
};
|
||||
|
||||
const severityColors: Record<ActivitySeverity, string> = {
|
||||
info: 'text-blue-500',
|
||||
success: 'text-green-500',
|
||||
warning: 'text-yellow-500',
|
||||
error: 'text-red-500',
|
||||
};
|
||||
|
||||
function formatRelativeTime(timestamp: string): string {
|
||||
const now = new Date();
|
||||
const date = new Date(timestamp);
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
export function RecentActivityFeed({ activities, maxItems = 10 }: RecentActivityFeedProps) {
|
||||
const navigate = useNavigate();
|
||||
const { upsertAndSetCurrentProject } = useAppStore();
|
||||
|
||||
const displayActivities = activities.slice(0, maxItems);
|
||||
|
||||
const handleActivityClick = useCallback(
|
||||
async (activity: RecentActivity) => {
|
||||
try {
|
||||
const initResult = await initializeProject(
|
||||
// We need to find the project path - use projectId as workaround
|
||||
// In real implementation, this would look up the path from projects list
|
||||
activity.projectId
|
||||
);
|
||||
|
||||
// Navigate to the project
|
||||
const projectPath = activity.projectId;
|
||||
const projectName = activity.projectName;
|
||||
|
||||
upsertAndSetCurrentProject(projectPath, projectName);
|
||||
|
||||
if (activity.featureId) {
|
||||
// Navigate to the specific feature
|
||||
navigate({ to: '/board', search: { featureId: activity.featureId } });
|
||||
} else {
|
||||
navigate({ to: '/board' });
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to navigate to activity', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
},
|
||||
[navigate, upsertAndSetCurrentProject]
|
||||
);
|
||||
|
||||
if (displayActivities.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
|
||||
<Clock className="w-8 h-8 mb-2 opacity-50" />
|
||||
<p className="text-sm">No recent activity</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{displayActivities.map((activity) => {
|
||||
const config = activityTypeConfig[activity.type];
|
||||
const Icon = config.icon;
|
||||
const iconColor = severityColors[activity.severity] || config.defaultColor;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={activity.id}
|
||||
className="group flex items-start gap-3 p-2 rounded-lg hover:bg-muted/50 cursor-pointer transition-colors"
|
||||
onClick={() => handleActivityClick(activity)}
|
||||
data-testid={`activity-item-${activity.id}`}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-full flex items-center justify-center shrink-0 mt-0.5',
|
||||
activity.severity === 'error' && 'bg-red-500/10',
|
||||
activity.severity === 'success' && 'bg-green-500/10',
|
||||
activity.severity === 'warning' && 'bg-yellow-500/10',
|
||||
activity.severity === 'info' && 'bg-blue-500/10'
|
||||
)}
|
||||
>
|
||||
<Icon className={cn('w-4 h-4', iconColor)} />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{activity.projectName}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground/50">
|
||||
{formatRelativeTime(activity.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-foreground truncate group-hover:text-brand-500 transition-colors">
|
||||
{activity.featureTitle || activity.description}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate mt-0.5">{config.label}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
127
apps/ui/src/components/views/overview/running-agents-panel.tsx
Normal file
127
apps/ui/src/components/views/overview/running-agents-panel.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* RunningAgentsPanel - Shows all currently running agents across projects
|
||||
*
|
||||
* Displays active AI agents with their status and quick access to features.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { initializeProject } from '@/lib/project-init';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ProjectStatus } from '@automaker/types';
|
||||
import { Bot, Activity, Folder, ArrowRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface RunningAgentsPanelProps {
|
||||
projects: ProjectStatus[];
|
||||
}
|
||||
|
||||
interface RunningAgent {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
projectPath: string;
|
||||
featureCount: number;
|
||||
isAutoMode: boolean;
|
||||
activeBranch?: string;
|
||||
}
|
||||
|
||||
export function RunningAgentsPanel({ projects }: RunningAgentsPanelProps) {
|
||||
const navigate = useNavigate();
|
||||
const { upsertAndSetCurrentProject } = useAppStore();
|
||||
|
||||
// Extract running agents from projects
|
||||
const runningAgents: RunningAgent[] = projects
|
||||
.filter((p) => p.isAutoModeRunning || p.featureCounts.running > 0)
|
||||
.map((p) => ({
|
||||
projectId: p.projectId,
|
||||
projectName: p.projectName,
|
||||
projectPath: p.projectPath,
|
||||
featureCount: p.featureCounts.running,
|
||||
isAutoMode: p.isAutoModeRunning,
|
||||
activeBranch: p.activeBranch,
|
||||
}));
|
||||
|
||||
const handleAgentClick = useCallback(
|
||||
async (agent: RunningAgent) => {
|
||||
try {
|
||||
const initResult = await initializeProject(agent.projectPath);
|
||||
if (!initResult.success) {
|
||||
toast.error('Failed to open project', {
|
||||
description: initResult.error || 'Unknown error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
upsertAndSetCurrentProject(agent.projectPath, agent.projectName);
|
||||
navigate({ to: '/board' });
|
||||
} catch (error) {
|
||||
toast.error('Failed to navigate to agent', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
},
|
||||
[navigate, upsertAndSetCurrentProject]
|
||||
);
|
||||
|
||||
if (runningAgents.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
|
||||
<Bot className="w-8 h-8 mb-2 opacity-50" />
|
||||
<p className="text-sm">No agents running</p>
|
||||
<p className="text-xs mt-1">Start auto-mode on a project to see activity here</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{runningAgents.map((agent) => (
|
||||
<div
|
||||
key={agent.projectId}
|
||||
className="group flex items-center gap-3 p-3 rounded-lg border border-green-500/20 bg-green-500/5 hover:bg-green-500/10 cursor-pointer transition-all"
|
||||
onClick={() => handleAgentClick(agent)}
|
||||
data-testid={`running-agent-${agent.projectId}`}
|
||||
>
|
||||
{/* Animated icon */}
|
||||
<div className="relative w-10 h-10 rounded-lg bg-green-500/20 flex items-center justify-center shrink-0">
|
||||
<Bot className="w-5 h-5 text-green-500" />
|
||||
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full animate-pulse" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-foreground truncate group-hover:text-green-500 transition-colors">
|
||||
{agent.projectName}
|
||||
</span>
|
||||
{agent.isAutoMode && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-500 font-medium">
|
||||
Auto
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground">
|
||||
{agent.featureCount > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Activity className="w-3 h-3" />
|
||||
{agent.featureCount} feature{agent.featureCount !== 1 ? 's' : ''} running
|
||||
</span>
|
||||
)}
|
||||
{agent.activeBranch && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Folder className="w-3 h-3" />
|
||||
{agent.activeBranch}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<ArrowRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user