Skip to content

Commit 583b1a7

Browse files
authored
Merge pull request #45 from codervisor/copilot/implement-phase-1-2
Implement AI Agent Observability Phase 1-2: Foundation Infrastructure & Session Management UI
2 parents f148ec3 + f62157b commit 583b1a7

File tree

16 files changed

+2042
-52
lines changed

16 files changed

+2042
-52
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* API endpoint for agent sessions
3+
*/
4+
5+
import { NextRequest, NextResponse } from 'next/server';
6+
import { AgentSessionService } from '@codervisor/devlog-core/server';
7+
8+
export async function GET(
9+
request: NextRequest,
10+
{ params }: { params: { name: string } }
11+
) {
12+
try {
13+
const projectName = params.name;
14+
const searchParams = request.nextUrl.searchParams;
15+
16+
// Parse query parameters
17+
const agentId = searchParams.get('agentId') || undefined;
18+
const outcome = searchParams.get('outcome') || undefined;
19+
const startTimeFrom = searchParams.get('startTimeFrom') || undefined;
20+
const startTimeTo = searchParams.get('startTimeTo') || undefined;
21+
const limit = parseInt(searchParams.get('limit') || '100');
22+
const offset = parseInt(searchParams.get('offset') || '0');
23+
24+
// Get project ID from name (simplified - in production, query from database)
25+
const projectId = 1; // TODO: Query project by name
26+
27+
const sessionService = AgentSessionService.getInstance(projectId);
28+
await sessionService.initialize();
29+
30+
const filter: any = { projectId, limit, offset };
31+
if (agentId) filter.agentId = agentId;
32+
if (outcome) filter.outcome = outcome;
33+
if (startTimeFrom) filter.startTimeFrom = new Date(startTimeFrom);
34+
if (startTimeTo) filter.startTimeTo = new Date(startTimeTo);
35+
36+
const sessions = await sessionService.listSessions(filter);
37+
38+
return NextResponse.json({
39+
success: true,
40+
data: sessions,
41+
pagination: {
42+
limit,
43+
offset,
44+
total: sessions.length,
45+
},
46+
});
47+
} catch (error) {
48+
console.error('Error fetching agent sessions:', error);
49+
return NextResponse.json(
50+
{
51+
success: false,
52+
error: error instanceof Error ? error.message : 'Failed to fetch agent sessions',
53+
},
54+
{ status: 500 }
55+
);
56+
}
57+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Agent Sessions Dashboard Page
3+
*
4+
* Displays all AI agent sessions for a project with filtering and search
5+
*/
6+
7+
import { Suspense } from 'react';
8+
import { SessionList } from '@/components/feature/agent-sessions/session-list';
9+
import { ActiveSessionsPanel } from '@/components/feature/agent-sessions/active-sessions-panel';
10+
11+
export default function AgentSessionsPage({ params }: { params: { name: string } }) {
12+
return (
13+
<div className="container mx-auto py-6 space-y-6">
14+
<div className="flex items-center justify-between">
15+
<div>
16+
<h1 className="text-3xl font-bold tracking-tight">Agent Sessions</h1>
17+
<p className="text-muted-foreground mt-2">
18+
Monitor and analyze AI coding agent activities for {params.name}
19+
</p>
20+
</div>
21+
</div>
22+
23+
{/* Active Sessions Panel */}
24+
<Suspense fallback={<div className="animate-pulse h-32 bg-gray-100 rounded-lg" />}>
25+
<ActiveSessionsPanel projectName={params.name} />
26+
</Suspense>
27+
28+
{/* Session History */}
29+
<div className="mt-8">
30+
<h2 className="text-2xl font-semibold mb-4">Session History</h2>
31+
<Suspense fallback={<div className="animate-pulse h-96 bg-gray-100 rounded-lg" />}>
32+
<SessionList projectName={params.name} />
33+
</Suspense>
34+
</div>
35+
</div>
36+
);
37+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Active Sessions Panel Component
3+
*
4+
* Displays currently active agent sessions with real-time updates
5+
*/
6+
7+
'use client';
8+
9+
import { useState, useEffect } from 'react';
10+
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
11+
import { Badge } from '@/components/ui/badge';
12+
import type { AgentSession } from '@codervisor/devlog-core';
13+
import { Activity } from 'lucide-react';
14+
15+
interface ActiveSessionsPanelProps {
16+
projectName: string;
17+
}
18+
19+
export function ActiveSessionsPanel({ projectName }: ActiveSessionsPanelProps) {
20+
const [activeSessions, setActiveSessions] = useState<AgentSession[]>([]);
21+
const [loading, setLoading] = useState(true);
22+
23+
useEffect(() => {
24+
async function fetchActiveSessions() {
25+
try {
26+
const response = await fetch(`/api/projects/${projectName}/agent-sessions?outcome=`);
27+
const data = await response.json();
28+
29+
if (data.success) {
30+
// Filter for sessions without endTime (active sessions)
31+
const active = (data.data || []).filter((s: AgentSession) => !s.endTime);
32+
setActiveSessions(active);
33+
}
34+
} catch (error) {
35+
console.error('Failed to fetch active sessions:', error);
36+
} finally {
37+
setLoading(false);
38+
}
39+
}
40+
41+
fetchActiveSessions();
42+
43+
// Refresh every 5 seconds
44+
const interval = setInterval(fetchActiveSessions, 5000);
45+
return () => clearInterval(interval);
46+
}, [projectName]);
47+
48+
if (loading) {
49+
return (
50+
<Card>
51+
<CardHeader>
52+
<CardTitle className="flex items-center gap-2">
53+
<Activity className="h-5 w-5 animate-pulse" />
54+
Active Sessions
55+
</CardTitle>
56+
</CardHeader>
57+
<CardContent>
58+
<div className="animate-pulse h-20 bg-gray-100 rounded" />
59+
</CardContent>
60+
</Card>
61+
);
62+
}
63+
64+
return (
65+
<Card>
66+
<CardHeader>
67+
<CardTitle className="flex items-center gap-2">
68+
<Activity className="h-5 w-5" />
69+
Active Sessions
70+
{activeSessions.length > 0 && (
71+
<Badge variant="secondary" className="ml-2">
72+
{activeSessions.length}
73+
</Badge>
74+
)}
75+
</CardTitle>
76+
</CardHeader>
77+
<CardContent>
78+
{activeSessions.length === 0 ? (
79+
<div className="text-center py-8 text-muted-foreground">
80+
<p>No active sessions</p>
81+
<p className="text-sm mt-1">AI agent sessions will appear here when they start</p>
82+
</div>
83+
) : (
84+
<div className="space-y-3">
85+
{activeSessions.map((session) => (
86+
<div
87+
key={session.id}
88+
className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg"
89+
>
90+
<div className="flex items-center gap-3">
91+
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse" />
92+
<div>
93+
<div className="font-medium">{session.agentId}</div>
94+
<div className="text-sm text-muted-foreground">
95+
{session.context.objective || 'In progress...'}
96+
</div>
97+
</div>
98+
</div>
99+
<div className="text-sm text-muted-foreground">
100+
{session.metrics.eventsCount || 0} events
101+
</div>
102+
</div>
103+
))}
104+
</div>
105+
)}
106+
</CardContent>
107+
</Card>
108+
);
109+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* Session Card Component
3+
*
4+
* Displays a single agent session with key metrics
5+
*/
6+
7+
'use client';
8+
9+
import Link from 'next/link';
10+
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
11+
import { Badge } from '@/components/ui/badge';
12+
import type { AgentSession } from '@codervisor/devlog-core';
13+
import { formatDistanceToNow } from 'date-fns';
14+
15+
interface SessionCardProps {
16+
session: AgentSession;
17+
projectName: string;
18+
}
19+
20+
const outcomeColors = {
21+
success: 'bg-green-100 text-green-800 border-green-200',
22+
partial: 'bg-yellow-100 text-yellow-800 border-yellow-200',
23+
failure: 'bg-red-100 text-red-800 border-red-200',
24+
abandoned: 'bg-gray-100 text-gray-800 border-gray-200',
25+
};
26+
27+
const agentNames = {
28+
'github-copilot': 'GitHub Copilot',
29+
'claude-code': 'Claude Code',
30+
'cursor': 'Cursor',
31+
'gemini-cli': 'Gemini CLI',
32+
'cline': 'Cline',
33+
'aider': 'Aider',
34+
'mcp-generic': 'MCP Generic',
35+
};
36+
37+
export function SessionCard({ session, projectName }: SessionCardProps) {
38+
const startTime = new Date(session.startTime);
39+
const timeAgo = formatDistanceToNow(startTime, { addSuffix: true });
40+
const duration = session.duration ? `${Math.floor(session.duration / 60)}m ${session.duration % 60}s` : 'In progress';
41+
42+
return (
43+
<Link href={`/projects/${projectName}/agent-sessions/${session.id}`}>
44+
<Card className="hover:shadow-md transition-shadow cursor-pointer">
45+
<CardHeader>
46+
<div className="flex items-start justify-between">
47+
<div className="flex-1">
48+
<CardTitle className="text-lg">
49+
{agentNames[session.agentId as keyof typeof agentNames] || session.agentId}
50+
</CardTitle>
51+
<CardDescription>
52+
Started {timeAgo}{duration}
53+
</CardDescription>
54+
</div>
55+
<div className="flex gap-2">
56+
{session.outcome && (
57+
<Badge
58+
variant="outline"
59+
className={outcomeColors[session.outcome as keyof typeof outcomeColors]}
60+
>
61+
{session.outcome}
62+
</Badge>
63+
)}
64+
{session.qualityScore !== undefined && (
65+
<Badge variant="outline" className="bg-blue-100 text-blue-800 border-blue-200">
66+
Quality: {Math.round(session.qualityScore)}%
67+
</Badge>
68+
)}
69+
</div>
70+
</div>
71+
</CardHeader>
72+
<CardContent>
73+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
74+
<div>
75+
<div className="text-muted-foreground">Events</div>
76+
<div className="font-semibold">{session.metrics.eventsCount || 0}</div>
77+
</div>
78+
<div>
79+
<div className="text-muted-foreground">Files Modified</div>
80+
<div className="font-semibold">{session.metrics.filesModified || 0}</div>
81+
</div>
82+
<div>
83+
<div className="text-muted-foreground">Lines Changed</div>
84+
<div className="font-semibold">
85+
+{session.metrics.linesAdded || 0} -{session.metrics.linesRemoved || 0}
86+
</div>
87+
</div>
88+
<div>
89+
<div className="text-muted-foreground">Tokens Used</div>
90+
<div className="font-semibold">
91+
{(session.metrics.tokensUsed || 0).toLocaleString()}
92+
</div>
93+
</div>
94+
</div>
95+
96+
{session.context.objective && (
97+
<div className="mt-4 text-sm text-muted-foreground">
98+
<span className="font-medium">Objective:</span> {session.context.objective}
99+
</div>
100+
)}
101+
</CardContent>
102+
</Card>
103+
</Link>
104+
);
105+
}

0 commit comments

Comments
 (0)