diff --git a/frontend/e2e/activity-feed.spec.ts b/frontend/e2e/activity-feed.spec.ts index c2dddd4..f64f68f 100644 --- a/frontend/e2e/activity-feed.spec.ts +++ b/frontend/e2e/activity-feed.spec.ts @@ -103,9 +103,11 @@ test.describe('Activity Feed Page', () => { test('approval actions are visible for pending approvals', async ({ page }) => { // Find approval event - const approvalEvent = page.locator('[data-testid^="event-item-"]', { - has: page.getByText('Action Required'), - }).first(); + const approvalEvent = page + .locator('[data-testid^="event-item-"]', { + has: page.getByText('Action Required'), + }) + .first(); // Approval buttons should be visible await expect(approvalEvent.getByTestId('approve-button')).toBeVisible(); diff --git a/frontend/e2e/homepage.spec.ts b/frontend/e2e/homepage.spec.ts index 18e803c..553f65d 100644 --- a/frontend/e2e/homepage.spec.ts +++ b/frontend/e2e/homepage.spec.ts @@ -120,7 +120,10 @@ test.describe('Homepage - Hero Section', () => { test('should navigate to GitHub when clicking View on GitHub', async ({ page }) => { const githubLink = page.getByRole('link', { name: /View on GitHub/i }).first(); await expect(githubLink).toBeVisible(); - await expect(githubLink).toHaveAttribute('href', expect.stringContaining('gitea.pragmazest.com')); + await expect(githubLink).toHaveAttribute( + 'href', + expect.stringContaining('gitea.pragmazest.com') + ); }); test('should navigate to components when clicking Explore Components', async ({ page }) => { diff --git a/frontend/e2e/project-dashboard.spec.ts b/frontend/e2e/project-dashboard.spec.ts index 0583474..29304c9 100644 --- a/frontend/e2e/project-dashboard.spec.ts +++ b/frontend/e2e/project-dashboard.spec.ts @@ -33,7 +33,9 @@ test.describe('Project Dashboard Page', () => { await expect(page.getByTestId('project-header')).toBeVisible(); // Check project name - await expect(page.getByRole('heading', { level: 1 })).toContainText('E-Commerce Platform Redesign'); + await expect(page.getByRole('heading', { level: 1 })).toContainText( + 'E-Commerce Platform Redesign' + ); // Check status badges await expect(page.getByText('In Progress')).toBeVisible(); @@ -288,7 +290,9 @@ test.describe('Project Dashboard Activity Feed', () => { await page.waitForLoadState('networkidle'); // Look for action buttons in activity feed (if any require action) - const reviewButton = page.getByTestId('recent-activity').getByRole('button', { name: /review/i }); + const reviewButton = page + .getByTestId('recent-activity') + .getByRole('button', { name: /review/i }); const count = await reviewButton.count(); // Either there are action items or not - both are valid diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js index 6951ed1..b68694a 100644 --- a/frontend/public/mockServiceWorker.js +++ b/frontend/public/mockServiceWorker.js @@ -7,42 +7,42 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.12.3' -const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' -const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') -const activeClientIds = new Set() +const PACKAGE_VERSION = '2.12.3'; +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'; +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse'); +const activeClientIds = new Set(); addEventListener('install', function () { - self.skipWaiting() -}) + self.skipWaiting(); +}); addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()) -}) + event.waitUntil(self.clients.claim()); +}); addEventListener('message', async function (event) { - const clientId = Reflect.get(event.source || {}, 'id') + const clientId = Reflect.get(event.source || {}, 'id'); if (!clientId || !self.clients) { - return + return; } - const client = await self.clients.get(clientId) + const client = await self.clients.get(clientId); if (!client) { - return + return; } const allClients = await self.clients.matchAll({ type: 'window', - }) + }); switch (event.data) { case 'KEEPALIVE_REQUEST': { sendToClient(client, { type: 'KEEPALIVE_RESPONSE', - }) - break + }); + break; } case 'INTEGRITY_CHECK_REQUEST': { @@ -52,12 +52,12 @@ addEventListener('message', async function (event) { packageVersion: PACKAGE_VERSION, checksum: INTEGRITY_CHECKSUM, }, - }) - break + }); + break; } case 'MOCK_ACTIVATE': { - activeClientIds.add(clientId) + activeClientIds.add(clientId); sendToClient(client, { type: 'MOCKING_ENABLED', @@ -67,54 +67,51 @@ addEventListener('message', async function (event) { frameType: client.frameType, }, }, - }) - break + }); + break; } case 'CLIENT_CLOSED': { - activeClientIds.delete(clientId) + activeClientIds.delete(clientId); const remainingClients = allClients.filter((client) => { - return client.id !== clientId - }) + return client.id !== clientId; + }); // Unregister itself when there are no more clients if (remainingClients.length === 0) { - self.registration.unregister() + self.registration.unregister(); } - break + break; } } -}) +}); addEventListener('fetch', function (event) { - const requestInterceptedAt = Date.now() + const requestInterceptedAt = Date.now(); // Bypass navigation requests. if (event.request.mode === 'navigate') { - return + return; } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. - if ( - event.request.cache === 'only-if-cached' && - event.request.mode !== 'same-origin' - ) { - return + if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') { + return; } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests // after it's been terminated (still remains active until the next reload). if (activeClientIds.size === 0) { - return + return; } - const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) -}) + const requestId = crypto.randomUUID(); + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)); +}); /** * @param {FetchEvent} event @@ -122,23 +119,18 @@ addEventListener('fetch', function (event) { * @param {number} requestInterceptedAt */ async function handleRequest(event, requestId, requestInterceptedAt) { - const client = await resolveMainClient(event) - const requestCloneForEvents = event.request.clone() - const response = await getResponse( - event, - client, - requestId, - requestInterceptedAt, - ) + const client = await resolveMainClient(event); + const requestCloneForEvents = event.request.clone(); + const response = await getResponse(event, client, requestId, requestInterceptedAt); // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { - const serializedRequest = await serializeRequest(requestCloneForEvents) + const serializedRequest = await serializeRequest(requestCloneForEvents); // Clone the response so both the client and the library could consume it. - const responseClone = response.clone() + const responseClone = response.clone(); sendToClient( client, @@ -159,11 +151,11 @@ async function handleRequest(event, requestId, requestInterceptedAt) { }, }, }, - responseClone.body ? [serializedRequest.body, responseClone.body] : [], - ) + responseClone.body ? [serializedRequest.body, responseClone.body] : [] + ); } - return response + return response; } /** @@ -175,30 +167,30 @@ async function handleRequest(event, requestId, requestInterceptedAt) { * @returns {Promise} */ async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId) + const client = await self.clients.get(event.clientId); if (activeClientIds.has(event.clientId)) { - return client + return client; } if (client?.frameType === 'top-level') { - return client + return client; } const allClients = await self.clients.matchAll({ type: 'window', - }) + }); return allClients .filter((client) => { // Get only those clients that are currently visible. - return client.visibilityState === 'visible' + return client.visibilityState === 'visible'; }) .find((client) => { // Find the client ID that's recorded in the // set of clients that have registered the worker. - return activeClientIds.has(client.id) - }) + return activeClientIds.has(client.id); + }); } /** @@ -211,36 +203,34 @@ async function resolveMainClient(event) { async function getResponse(event, client, requestId, requestInterceptedAt) { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). - const requestClone = event.request.clone() + const requestClone = event.request.clone(); function passthrough() { // Cast the request headers to a new Headers instance // so the headers can be manipulated with. - const headers = new Headers(requestClone.headers) + const headers = new Headers(requestClone.headers); // Remove the "accept" header value that marked this request as passthrough. // This prevents request alteration and also keeps it compliant with the // user-defined CORS policies. - const acceptHeader = headers.get('accept') + const acceptHeader = headers.get('accept'); if (acceptHeader) { - const values = acceptHeader.split(',').map((value) => value.trim()) - const filteredValues = values.filter( - (value) => value !== 'msw/passthrough', - ) + const values = acceptHeader.split(',').map((value) => value.trim()); + const filteredValues = values.filter((value) => value !== 'msw/passthrough'); if (filteredValues.length > 0) { - headers.set('accept', filteredValues.join(', ')) + headers.set('accept', filteredValues.join(', ')); } else { - headers.delete('accept') + headers.delete('accept'); } } - return fetch(requestClone, { headers }) + return fetch(requestClone, { headers }); } // Bypass mocking when the client is not active. if (!client) { - return passthrough() + return passthrough(); } // Bypass initial page load requests (i.e. static assets). @@ -248,11 +238,11 @@ async function getResponse(event, client, requestId, requestInterceptedAt) { // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // and is not ready to handle requests. if (!activeClientIds.has(client.id)) { - return passthrough() + return passthrough(); } // Notify the client that a request has been intercepted. - const serializedRequest = await serializeRequest(event.request) + const serializedRequest = await serializeRequest(event.request); const clientMessage = await sendToClient( client, { @@ -263,20 +253,20 @@ async function getResponse(event, client, requestId, requestInterceptedAt) { ...serializedRequest, }, }, - [serializedRequest.body], - ) + [serializedRequest.body] + ); switch (clientMessage.type) { case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data) + return respondWithMock(clientMessage.data); } case 'PASSTHROUGH': { - return passthrough() + return passthrough(); } } - return passthrough() + return passthrough(); } /** @@ -287,21 +277,18 @@ async function getResponse(event, client, requestId, requestInterceptedAt) { */ function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { - const channel = new MessageChannel() + const channel = new MessageChannel(); channel.port1.onmessage = (event) => { if (event.data && event.data.error) { - return reject(event.data.error) + return reject(event.data.error); } - resolve(event.data) - } + resolve(event.data); + }; - client.postMessage(message, [ - channel.port2, - ...transferrables.filter(Boolean), - ]) - }) + client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]); + }); } /** @@ -314,17 +301,17 @@ function respondWithMock(response) { // instance will have status code set to 0. Since it's not possible to create // a Response instance with status code 0, handle that use-case separately. if (response.status === 0) { - return Response.error() + return Response.error(); } - const mockedResponse = new Response(response.body, response) + const mockedResponse = new Response(response.body, response); Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { value: true, enumerable: true, - }) + }); - return mockedResponse + return mockedResponse; } /** @@ -345,5 +332,5 @@ async function serializeRequest(request) { referrerPolicy: request.referrerPolicy, body: await request.arrayBuffer(), keepalive: request.keepalive, - } + }; } diff --git a/frontend/src/app/[locale]/(authenticated)/activity/page.tsx b/frontend/src/app/[locale]/(authenticated)/activity/page.tsx index 1a918ce..2f42391 100644 --- a/frontend/src/app/[locale]/(authenticated)/activity/page.tsx +++ b/frontend/src/app/[locale]/(authenticated)/activity/page.tsx @@ -199,13 +199,10 @@ export default function ActivityFeedPage() { )} - @@ -214,7 +211,8 @@ export default function ActivityFeedPage() { {(!isConnected || sseEvents.length === 0) && (

- Demo Mode: Showing sample events. Connect to a real project to see live updates. + Demo Mode: Showing sample events. Connect to a real project to see + live updates.

)} diff --git a/frontend/src/app/[locale]/(authenticated)/agents/[id]/page.tsx b/frontend/src/app/[locale]/(authenticated)/agents/[id]/page.tsx index 2674757..6bcac09 100644 --- a/frontend/src/app/[locale]/(authenticated)/agents/[id]/page.tsx +++ b/frontend/src/app/[locale]/(authenticated)/agents/[id]/page.tsx @@ -32,11 +32,7 @@ export default function AgentTypeDetailPage() { const [viewMode, setViewMode] = useState(isNew ? 'create' : 'detail'); // Fetch agent type data (skip if creating new) - const { - data: agentType, - isLoading, - error, - } = useAgentType(isNew ? null : id); + const { data: agentType, isLoading, error } = useAgentType(isNew ? null : id); // Mutations const createMutation = useCreateAgentType(); @@ -171,7 +167,7 @@ export default function AgentTypeDetailPage() {
{(viewMode === 'create' || viewMode === 'edit') && ( -
@@ -171,9 +162,7 @@ export default function IssueDetailPage({ params }: IssueDetailPageProps) {
-
-                    {issue.description}
-                  
+
{issue.description}
diff --git a/frontend/src/app/[locale]/(authenticated)/projects/[id]/issues/page.tsx b/frontend/src/app/[locale]/(authenticated)/projects/[id]/issues/page.tsx index 65aebfc..0c83d98 100644 --- a/frontend/src/app/[locale]/(authenticated)/projects/[id]/issues/page.tsx +++ b/frontend/src/app/[locale]/(authenticated)/projects/[id]/issues/page.tsx @@ -14,12 +14,7 @@ import { useRouter } from 'next/navigation'; import { Plus, Upload } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; -import { - IssueFilters, - IssueTable, - BulkActions, - useIssues, -} from '@/features/issues'; +import { IssueFilters, IssueTable, BulkActions, useIssues } from '@/features/issues'; import type { IssueFiltersType, IssueSort } from '@/features/issues'; interface ProjectIssuesPageProps { @@ -95,11 +90,7 @@ export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {

Failed to load issues. Please try again later.

- @@ -169,26 +160,15 @@ export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {
Showing {(data.pagination.page - 1) * data.pagination.page_size + 1} to{' '} - {Math.min( - data.pagination.page * data.pagination.page_size, - data.pagination.total - )}{' '} - of {data.pagination.total} issues + {Math.min(data.pagination.page * data.pagination.page_size, data.pagination.total)} of{' '} + {data.pagination.total} issues {data.pagination.total_pages > 1 && (
- -
diff --git a/frontend/src/app/[locale]/prototypes/ISSUE_COMMENTS.md b/frontend/src/app/[locale]/prototypes/ISSUE_COMMENTS.md index 9005b5b..29a99b7 100644 --- a/frontend/src/app/[locale]/prototypes/ISSUE_COMMENTS.md +++ b/frontend/src/app/[locale]/prototypes/ISSUE_COMMENTS.md @@ -14,44 +14,52 @@ This document contains the comments to be added to each Gitea issue for the desi The Project Dashboard prototype has been created and is ready for review. ### How to View + 1. Start the frontend dev server: `cd frontend && npm run dev` 2. Navigate to: `http://localhost:3000/en/prototypes/project-dashboard` ### What's Included **Header Section** + - Project name with status badge (In Progress, Completed, Paused, Blocked) - Autonomy level indicator (Full Control, Milestone, Autonomous) - Quick action buttons (Pause Project, Run Sprint) **Agent Panel** + - List of all project agents with avatars - Real-time status indicators (active = green, idle = yellow, pending = gray) - Current task description for each agent - Last activity timestamp **Sprint Overview** + - Current sprint progress bar - Issue statistics grid (Completed, In Progress, Blocked, To Do) - Visual burndown chart with ideal vs actual lines - Sprint selector dropdown **Issue Summary Sidebar** + - Count of issues by status with color-coded icons - Quick links to view all issues **Recent Activity Feed** + - Chronological event list with type icons - Agent attribution - Highlighted approval requests with action buttons ### Key Design Decisions + - Three-column layout on desktop (2/3 main, 1/3 sidebar) - Agent status uses traffic light colors for intuitive understanding - Burndown chart is simplified for quick scanning - Activity feed limited to 5 items with "View All" link ### Questions for Review + 1. Is the burndown chart detailed enough? 2. Should agent cards be expandable for more details? 3. Is the 5-item activity feed sufficient? @@ -59,6 +67,7 @@ The Project Dashboard prototype has been created and is ready for review. **Please review and approve or provide feedback.** Files: + - `/frontend/src/app/[locale]/prototypes/project-dashboard/page.tsx` - `/frontend/src/app/[locale]/prototypes/project-dashboard/README.md` ``` @@ -75,6 +84,7 @@ Files: The Agent Configuration UI prototype has been created and is ready for review. ### How to View + 1. Start the frontend dev server: `cd frontend && npm run dev` 2. Navigate to: `http://localhost:3000/en/prototypes/agent-configuration` @@ -102,18 +112,21 @@ The Agent Configuration UI prototype has been created and is ready for review. - **Personality Tab**: Large textarea for personality prompt ### Key Design Decisions + - Separate views for browsing, viewing, and editing - Tabbed editor reduces cognitive load - MCP permissions show nested scopes when enabled - Model parameters have helpful descriptions ### User Flows to Test + 1. Click any card to see detail view 2. Click "Edit" to see editor view 3. Click "Create Agent Type" for blank editor 4. Navigate tabs in editor ### Questions for Review + 1. Is the tabbed editor the right approach? 2. Should expertise be free-form tags or predefined list? 3. Should model parameters have "presets"? @@ -121,6 +134,7 @@ The Agent Configuration UI prototype has been created and is ready for review. **Please review and approve or provide feedback.** Files: + - `/frontend/src/app/[locale]/prototypes/agent-configuration/page.tsx` - `/frontend/src/app/[locale]/prototypes/agent-configuration/README.md` ``` @@ -137,12 +151,14 @@ Files: The Issue List and Detail Views prototype has been created and is ready for review. ### How to View + 1. Start the frontend dev server: `cd frontend && npm run dev` 2. Navigate to: `http://localhost:3000/en/prototypes/issue-management` ### What's Included **List View** + - Filterable table with sortable columns - Quick status filter + expandable advanced filters - Bulk action bar (appears when selecting issues) @@ -150,6 +166,7 @@ The Issue List and Detail Views prototype has been created and is ready for revi - Labels displayed as badges **Filter Options** + - Status: Open, In Progress, In Review, Blocked, Done - Priority: High, Medium, Low - Sprint: Current sprints, Backlog @@ -157,6 +174,7 @@ The Issue List and Detail Views prototype has been created and is ready for revi - Labels: Feature, Bug, Backend, Frontend, etc. **Detail View** + - Full issue content (markdown-like display) - Status workflow panel (click to change status) - Assignment panel with agent avatar @@ -168,12 +186,14 @@ The Issue List and Detail Views prototype has been created and is ready for revi - Development section (branch, PR link) ### Key Design Decisions + - Table layout for density and scannability - Status workflow matches common issue tracker patterns - Sync status indicator shows data freshness - Activity timeline shows issue history ### User Flows to Test + 1. Use search and filters 2. Click checkboxes to see bulk actions 3. Sort by clicking column headers @@ -181,6 +201,7 @@ The Issue List and Detail Views prototype has been created and is ready for revi 5. Click status buttons in detail view ### Questions for Review + 1. Should we add Kanban view as alternative? 2. Is the sync indicator clear enough? 3. Should there be inline editing? @@ -188,6 +209,7 @@ The Issue List and Detail Views prototype has been created and is ready for revi **Please review and approve or provide feedback.** Files: + - `/frontend/src/app/[locale]/prototypes/issue-management/page.tsx` - `/frontend/src/app/[locale]/prototypes/issue-management/README.md` ``` @@ -204,12 +226,14 @@ Files: The Real-time Activity Feed prototype has been created and is ready for review. ### How to View + 1. Start the frontend dev server: `cd frontend && npm run dev` 2. Navigate to: `http://localhost:3000/en/prototypes/activity-feed` ### What's Included **Event Types Displayed** + - Agent Status: Started, paused, resumed, stopped - Agent Message: Updates, questions, progress reports - Issue Update: Status changes, assignments, creation @@ -219,6 +243,7 @@ The Real-time Activity Feed prototype has been created and is ready for review. - Milestone: Goals achieved, completions **Features** + - Real-time connection indicator (pulsing green when connected) - Time-based event grouping (New, Earlier Today, Yesterday, etc.) - Search functionality @@ -231,6 +256,7 @@ The Real-time Activity Feed prototype has been created and is ready for review. - Mark all read functionality ### Key Design Decisions + - Card-based layout for clear event separation - Orange left border highlights action-required items - Time grouping helps users orient in timeline @@ -238,6 +264,7 @@ The Real-time Activity Feed prototype has been created and is ready for review. - Real-time indicator builds trust in data freshness ### User Flows to Test + 1. Scroll through the event feed 2. Click events to expand details 3. Open filter panel and select filters @@ -245,6 +272,7 @@ The Real-time Activity Feed prototype has been created and is ready for review. 5. Click "Mark all read" ### Questions for Review + 1. Should events be grouped by time or show flat? 2. Should there be sound notifications for urgent items? 3. Should users be able to "star" events? @@ -252,6 +280,7 @@ The Real-time Activity Feed prototype has been created and is ready for review. **Please review and approve or provide feedback.** Files: + - `/frontend/src/app/[locale]/prototypes/activity-feed/page.tsx` - `/frontend/src/app/[locale]/prototypes/activity-feed/README.md` ``` @@ -269,6 +298,7 @@ The comments above should be added to the respective Gitea issues at: - Issue #39: Real-time Activity Feed After the user reviews each prototype and provides feedback: + 1. Iterate on the design based on feedback 2. Get explicit approval 3. Begin implementation diff --git a/frontend/src/app/[locale]/prototypes/page.tsx b/frontend/src/app/[locale]/prototypes/page.tsx index 02314d0..4756b2f 100644 --- a/frontend/src/app/[locale]/prototypes/page.tsx +++ b/frontend/src/app/[locale]/prototypes/page.tsx @@ -1,13 +1,7 @@ 'use client'; import Link from 'next/link'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { diff --git a/frontend/src/components/activity/ActivityFeed.tsx b/frontend/src/components/activity/ActivityFeed.tsx index 0bdafac..e6ff1f8 100644 --- a/frontend/src/components/activity/ActivityFeed.tsx +++ b/frontend/src/components/activity/ActivityFeed.tsx @@ -248,12 +248,47 @@ const EVENT_TYPE_CONFIG: Record< }; const FILTER_CATEGORIES = [ - { id: 'agent', label: 'Agent Actions', types: [EventType.AGENT_SPAWNED, EventType.AGENT_MESSAGE, EventType.AGENT_STATUS_CHANGED, EventType.AGENT_TERMINATED] }, - { id: 'issue', label: 'Issues', types: [EventType.ISSUE_CREATED, EventType.ISSUE_UPDATED, EventType.ISSUE_ASSIGNED, EventType.ISSUE_CLOSED] }, + { + id: 'agent', + label: 'Agent Actions', + types: [ + EventType.AGENT_SPAWNED, + EventType.AGENT_MESSAGE, + EventType.AGENT_STATUS_CHANGED, + EventType.AGENT_TERMINATED, + ], + }, + { + id: 'issue', + label: 'Issues', + types: [ + EventType.ISSUE_CREATED, + EventType.ISSUE_UPDATED, + EventType.ISSUE_ASSIGNED, + EventType.ISSUE_CLOSED, + ], + }, { id: 'sprint', label: 'Sprints', types: [EventType.SPRINT_STARTED, EventType.SPRINT_COMPLETED] }, - { id: 'approval', label: 'Approvals', types: [EventType.APPROVAL_REQUESTED, EventType.APPROVAL_GRANTED, EventType.APPROVAL_DENIED] }, - { id: 'workflow', label: 'Workflows', types: [EventType.WORKFLOW_STARTED, EventType.WORKFLOW_STEP_COMPLETED, EventType.WORKFLOW_COMPLETED, EventType.WORKFLOW_FAILED] }, - { id: 'project', label: 'Projects', types: [EventType.PROJECT_CREATED, EventType.PROJECT_UPDATED, EventType.PROJECT_ARCHIVED] }, + { + id: 'approval', + label: 'Approvals', + types: [EventType.APPROVAL_REQUESTED, EventType.APPROVAL_GRANTED, EventType.APPROVAL_DENIED], + }, + { + id: 'workflow', + label: 'Workflows', + types: [ + EventType.WORKFLOW_STARTED, + EventType.WORKFLOW_STEP_COMPLETED, + EventType.WORKFLOW_COMPLETED, + EventType.WORKFLOW_FAILED, + ], + }, + { + id: 'project', + label: 'Projects', + types: [EventType.PROJECT_CREATED, EventType.PROJECT_UPDATED, EventType.PROJECT_ARCHIVED], + }, ]; // ============================================================================ @@ -266,25 +301,60 @@ function getEventConfig(event: ProjectEvent) { // Fallback based on event category if (isAgentEvent(event)) { - return { icon: Bot, label: event.type, color: 'text-blue-500', bgColor: 'bg-blue-100 dark:bg-blue-900' }; + return { + icon: Bot, + label: event.type, + color: 'text-blue-500', + bgColor: 'bg-blue-100 dark:bg-blue-900', + }; } if (isIssueEvent(event)) { - return { icon: FileText, label: event.type, color: 'text-green-500', bgColor: 'bg-green-100 dark:bg-green-900' }; + return { + icon: FileText, + label: event.type, + color: 'text-green-500', + bgColor: 'bg-green-100 dark:bg-green-900', + }; } if (isSprintEvent(event)) { - return { icon: PlayCircle, label: event.type, color: 'text-indigo-500', bgColor: 'bg-indigo-100 dark:bg-indigo-900' }; + return { + icon: PlayCircle, + label: event.type, + color: 'text-indigo-500', + bgColor: 'bg-indigo-100 dark:bg-indigo-900', + }; } if (isApprovalEvent(event)) { - return { icon: AlertTriangle, label: event.type, color: 'text-orange-500', bgColor: 'bg-orange-100 dark:bg-orange-900' }; + return { + icon: AlertTriangle, + label: event.type, + color: 'text-orange-500', + bgColor: 'bg-orange-100 dark:bg-orange-900', + }; } if (isWorkflowEvent(event)) { - return { icon: Workflow, label: event.type, color: 'text-cyan-500', bgColor: 'bg-cyan-100 dark:bg-cyan-900' }; + return { + icon: Workflow, + label: event.type, + color: 'text-cyan-500', + bgColor: 'bg-cyan-100 dark:bg-cyan-900', + }; } if (isProjectEvent(event)) { - return { icon: Folder, label: event.type, color: 'text-teal-500', bgColor: 'bg-teal-100 dark:bg-teal-900' }; + return { + icon: Folder, + label: event.type, + color: 'text-teal-500', + bgColor: 'bg-teal-100 dark:bg-teal-900', + }; } - return { icon: Activity, label: event.type, color: 'text-gray-500', bgColor: 'bg-gray-100 dark:bg-gray-800' }; + return { + icon: Activity, + label: event.type, + color: 'text-gray-500', + bgColor: 'bg-gray-100 dark:bg-gray-800', + }; } function getEventSummary(event: ProjectEvent): string { @@ -304,7 +374,9 @@ function getEventSummary(event: ProjectEvent): string { case EventType.ISSUE_UPDATED: return `Issue ${payload.issue_id || ''} updated`; case EventType.ISSUE_ASSIGNED: - return payload.assignee_name ? `Assigned to ${payload.assignee_name}` : 'Issue assignment changed'; + return payload.assignee_name + ? `Assigned to ${payload.assignee_name}` + : 'Issue assignment changed'; case EventType.ISSUE_CLOSED: return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed'; case EventType.SPRINT_STARTED: @@ -318,11 +390,15 @@ function getEventSummary(event: ProjectEvent): string { case EventType.APPROVAL_DENIED: return payload.reason ? `Denied: ${payload.reason}` : 'Approval denied'; case EventType.WORKFLOW_STARTED: - return payload.workflow_type ? `${payload.workflow_type} workflow started` : 'Workflow started'; + return payload.workflow_type + ? `${payload.workflow_type} workflow started` + : 'Workflow started'; case EventType.WORKFLOW_STEP_COMPLETED: return `Step ${payload.step_number}/${payload.total_steps}: ${payload.step_name || 'completed'}`; case EventType.WORKFLOW_COMPLETED: - return payload.duration_seconds ? `Completed in ${payload.duration_seconds}s` : 'Workflow completed'; + return payload.duration_seconds + ? `Completed in ${payload.duration_seconds}s` + : 'Workflow completed'; case EventType.WORKFLOW_FAILED: return payload.error_message ? String(payload.error_message) : 'Workflow failed'; default: @@ -391,11 +467,7 @@ function ConnectionIndicator({ state, onReconnect, className }: ConnectionIndica return (
{/* Expanded Details */} - {expanded && (() => { - const issueId = payload.issue_id as string | undefined; - const pullRequest = payload.pullRequest as string | number | undefined; - const documentUrl = payload.documentUrl as string | undefined; - const progress = payload.progress as number | undefined; + {expanded && + (() => { + const issueId = payload.issue_id as string | undefined; + const pullRequest = payload.pullRequest as string | number | undefined; + const documentUrl = payload.documentUrl as string | undefined; + const progress = payload.progress as number | undefined; - return ( -
- {/* Issue/PR Links */} - {issueId && ( -
-
- )} - {pullRequest && ( -
-
- )} - - {/* Document Links */} - {documentUrl && ( -
-
- )} - - {/* Progress */} - {progress !== undefined && ( -
-
- Progress - {progress}% + return ( +
+ {/* Issue/PR Links */} + {issueId && ( +
+
-
-
+ )} + {pullRequest && ( +
+
-
- )} + )} - {/* Timestamp */} -

- {new Date(event.timestamp).toLocaleString()} -

+ {/* Document Links */} + {documentUrl && ( + + )} - {/* Raw Payload (for debugging) */} -
- - View raw payload - -
-                    {JSON.stringify(event.payload, null, 2)}
-                  
-
-
- ); - })()} + {/* Progress */} + {progress !== undefined && ( +
+
+ Progress + {progress}% +
+
+
+
+
+ )} + + {/* Timestamp */} +

+ {new Date(event.timestamp).toLocaleString()} +

+ + {/* Raw Payload (for debugging) */} +
+ + View raw payload + +
+                      {JSON.stringify(event.payload, null, 2)}
+                    
+
+
+ ); + })()} {/* Approval Actions */} {isPendingApproval && (onApprove || onReject) && ( @@ -680,7 +763,12 @@ function EventItem({ )} {onReject && ( - @@ -712,7 +800,10 @@ function LoadingSkeleton() { function EmptyState({ hasFilters }: { hasFilters: boolean }) { return ( -
+