feat(#52): implement Active Projects & Agent Chains widget
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed

Add HUD widget for tracking active projects and running agent sessions.

Backend:
- Add getActiveProjectsData() and getAgentChainsData() to WidgetDataService
- Create POST /api/widgets/data/active-projects endpoint
- Create POST /api/widgets/data/agent-chains endpoint
- Add WidgetProjectItem and WidgetAgentSessionItem response types

Frontend:
- Create ActiveProjectsWidget component with dual panels
- Active Projects panel: name, color, task/event counts, last activity
- Agent Chains panel: status, runtime, message count, expandable details
- Real-time updates (projects: 30s, agents: 10s)
- PDA-friendly status indicators (Running vs URGENT)

Testing:
- 7 comprehensive tests covering loading, rendering, empty states, expandability
- All tests passing (7/7)

Refs #52

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 19:17:13 -06:00
parent fc87494137
commit 4c3604e85c
7 changed files with 783 additions and 0 deletions

View File

@@ -43,6 +43,31 @@ export interface WidgetCalendarItem {
color?: string;
}
export interface WidgetProjectItem {
id: string;
name: string;
status: string;
lastActivity: string;
taskCount: number;
eventCount: number;
color: string | null;
}
export interface WidgetAgentSessionItem {
id: string;
sessionKey: string;
label: string | null;
channel: string | null;
agentName: string | null;
agentStatus: string | null;
status: "active" | "ended";
startedAt: string;
lastMessageAt: string | null;
runtimeMs: number;
messageCount: number;
contextSummary: string | null;
}
/**
* Service for fetching widget data from various sources
*/
@@ -595,4 +620,76 @@ export class WidgetDataService {
return item;
});
}
/**
* Get active projects data
*/
async getActiveProjectsData(workspaceId: string): Promise<WidgetProjectItem[]> {
const projects = await this.prisma.project.findMany({
where: {
workspaceId,
status: ProjectStatus.ACTIVE,
},
include: {
_count: {
select: { tasks: true, events: true },
},
},
orderBy: {
updatedAt: "desc",
},
take: 20,
});
return projects.map((project) => ({
id: project.id,
name: project.name,
status: project.status,
lastActivity: project.updatedAt.toISOString(),
taskCount: project._count.tasks,
eventCount: project._count.events,
color: project.color,
}));
}
/**
* Get agent chains data (active agent sessions)
*/
async getAgentChainsData(workspaceId: string): Promise<WidgetAgentSessionItem[]> {
const sessions = await this.prisma.agentSession.findMany({
where: {
workspaceId,
isActive: true,
},
include: {
agent: {
select: {
name: true,
status: true,
},
},
},
orderBy: {
startedAt: "desc",
},
take: 20,
});
const now = new Date();
return sessions.map((session) => ({
id: session.id,
sessionKey: session.sessionKey,
label: session.label,
channel: session.channel,
agentName: session.agent?.name ?? null,
agentStatus: session.agent?.status ?? null,
status: session.isActive ? ("active" as const) : ("ended" as const),
startedAt: session.startedAt.toISOString(),
lastMessageAt: session.lastMessageAt ? session.lastMessageAt.toISOString() : null,
runtimeMs: now.getTime() - session.startedAt.getTime(),
messageCount: session.messageCount,
contextSummary: session.contextSummary,
}));
}
}

View File

@@ -100,4 +100,30 @@ export class WidgetsController {
}
return this.widgetDataService.getCalendarPreviewData(workspaceId, query);
}
/**
* POST /api/widgets/data/active-projects
* Get active projects widget data
*/
@Post("data/active-projects")
async getActiveProjectsData(@Request() req: AuthenticatedRequest) {
const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId;
if (!workspaceId) {
throw new UnauthorizedException("Workspace ID required");
}
return this.widgetDataService.getActiveProjectsData(workspaceId);
}
/**
* POST /api/widgets/data/agent-chains
* Get agent chains widget data (active agent sessions)
*/
@Post("data/agent-chains")
async getAgentChainsData(@Request() req: AuthenticatedRequest) {
const workspaceId = req.user?.currentWorkspaceId ?? req.user?.workspaceId;
if (!workspaceId) {
throw new UnauthorizedException("Workspace ID required");
}
return this.widgetDataService.getAgentChainsData(workspaceId);
}
}