refactor(frontend): clean up code by consolidating multi-line JSX into single lines where feasible

- Refactored JSX elements to improve readability by collapsing multi-line props and attributes into single lines if their length permits.
- Improved consistency in component imports by grouping and consolidating them.
- No functional changes, purely restructuring for clarity and maintainability.
This commit is contained in:
2026-01-01 11:46:57 +01:00
parent a7ba0f9bd8
commit a4c91cb8c3
77 changed files with 600 additions and 907 deletions

View File

@@ -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();

View File

@@ -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 }) => {

View File

@@ -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

View File

@@ -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<Client | undefined>}
*/
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,
}
};
}

View File

@@ -199,13 +199,10 @@ export default function ActivityFeedPage() {
<BellOff className="h-4 w-4" />
)}
</Button>
<Button
variant="ghost"
size="icon"
onClick={reconnect}
aria-label="Refresh connection"
>
<RefreshCw className={connectionState === 'connecting' ? 'h-4 w-4 animate-spin' : 'h-4 w-4'} />
<Button variant="ghost" size="icon" onClick={reconnect} aria-label="Refresh connection">
<RefreshCw
className={connectionState === 'connecting' ? 'h-4 w-4 animate-spin' : 'h-4 w-4'}
/>
</Button>
</div>
</div>
@@ -214,7 +211,8 @@ export default function ActivityFeedPage() {
{(!isConnected || sseEvents.length === 0) && (
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-800 dark:bg-yellow-950">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Demo Mode:</strong> Showing sample events. Connect to a real project to see live updates.
<strong>Demo Mode:</strong> Showing sample events. Connect to a real project to see
live updates.
</p>
</div>
)}

View File

@@ -32,11 +32,7 @@ export default function AgentTypeDetailPage() {
const [viewMode, setViewMode] = useState<ViewMode>(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() {
<div className="container mx-auto px-4 py-6">
{(viewMode === 'create' || viewMode === 'edit') && (
<AgentTypeForm
agentType={viewMode === 'edit' ? agentType ?? undefined : undefined}
agentType={viewMode === 'edit' ? (agentType ?? undefined) : undefined}
onSubmit={handleSubmit}
onCancel={handleCancel}
isSubmitting={createMutation.isPending || updateMutation.isPending}

View File

@@ -10,13 +10,7 @@
import { use } from 'react';
import Link from 'next/link';
import {
ArrowLeft,
Calendar,
Clock,
ExternalLink,
Edit,
} from 'lucide-react';
import { ArrowLeft, Calendar, Clock, ExternalLink, Edit } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
@@ -65,10 +59,7 @@ export default function IssueDetailPage({ params }: IssueDetailPageProps) {
<Link href={`/${locale}/projects/${projectId}/issues`}>
<Button variant="outline">Back to Issues</Button>
</Link>
<Button
variant="outline"
onClick={() => window.location.reload()}
>
<Button variant="outline" onClick={() => window.location.reload()}>
Retry
</Button>
</div>
@@ -171,9 +162,7 @@ export default function IssueDetailPage({ params }: IssueDetailPageProps) {
</CardHeader>
<CardContent>
<div className="prose prose-sm max-w-none dark:prose-invert">
<pre className="whitespace-pre-wrap font-sans text-sm">
{issue.description}
</pre>
<pre className="whitespace-pre-wrap font-sans text-sm">{issue.description}</pre>
</div>
</CardContent>
</Card>

View File

@@ -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) {
<p className="mt-2 text-sm text-muted-foreground">
Failed to load issues. Please try again later.
</p>
<Button
variant="outline"
className="mt-4"
onClick={() => window.location.reload()}
>
<Button variant="outline" className="mt-4" onClick={() => window.location.reload()}>
Retry
</Button>
</div>
@@ -169,26 +160,15 @@ export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>
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
</span>
{data.pagination.total_pages > 1 && (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={!data.pagination.has_prev}
>
<Button variant="outline" size="sm" disabled={!data.pagination.has_prev}>
Previous
</Button>
<Button
variant="outline"
size="sm"
disabled={!data.pagination.has_next}
>
<Button variant="outline" size="sm" disabled={!data.pagination.has_next}>
Next
</Button>
</div>

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 (
<div className={cn('flex items-center gap-2', className)} data-testid="connection-indicator">
<span
className={cn(
'h-2 w-2 rounded-full',
config.color,
config.pulse && 'animate-pulse'
)}
className={cn('h-2 w-2 rounded-full', config.color, config.pulse && 'animate-pulse')}
aria-hidden="true"
/>
<span className="text-sm text-muted-foreground">{config.label}</span>
@@ -475,7 +547,10 @@ function FilterPanel({
checked={showPendingOnly}
onCheckedChange={(checked) => onShowPendingOnlyChange(checked as boolean)}
/>
<Label htmlFor="filter-pending" className="flex items-center gap-1 text-sm font-normal cursor-pointer">
<Label
htmlFor="filter-pending"
className="flex items-center gap-1 text-sm font-normal cursor-pointer"
>
Show only pending approvals
{pendingCount > 0 && (
<Badge variant="destructive" className="text-xs">
@@ -598,77 +673,85 @@ function EventItem({
}}
aria-label={expanded ? 'Collapse details' : 'Expand details'}
>
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
{expanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
</div>
</div>
{/* 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 (
<div className="mt-3 rounded-md bg-muted/50 p-3 space-y-3" data-testid="event-details">
{/* Issue/PR Links */}
{issueId && (
<div className="flex items-center gap-2 text-sm">
<CircleDot className="h-4 w-4" aria-hidden="true" />
<span>Issue #{issueId}</span>
</div>
)}
{pullRequest && (
<div className="flex items-center gap-2 text-sm">
<GitPullRequest className="h-4 w-4" aria-hidden="true" />
<span>PR #{String(pullRequest)}</span>
</div>
)}
{/* Document Links */}
{documentUrl && (
<div className="flex items-center gap-2 text-sm">
<ExternalLink className="h-4 w-4" aria-hidden="true" />
<a href={documentUrl} className="text-primary hover:underline">
{documentUrl}
</a>
</div>
)}
{/* Progress */}
{progress !== undefined && (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span>Progress</span>
<span>{progress}%</span>
return (
<div
className="mt-3 rounded-md bg-muted/50 p-3 space-y-3"
data-testid="event-details"
>
{/* Issue/PR Links */}
{issueId && (
<div className="flex items-center gap-2 text-sm">
<CircleDot className="h-4 w-4" aria-hidden="true" />
<span>Issue #{issueId}</span>
</div>
<div className="h-2 rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary"
style={{ width: `${progress}%` }}
/>
)}
{pullRequest && (
<div className="flex items-center gap-2 text-sm">
<GitPullRequest className="h-4 w-4" aria-hidden="true" />
<span>PR #{String(pullRequest)}</span>
</div>
</div>
)}
)}
{/* Timestamp */}
<p className="text-xs text-muted-foreground">
{new Date(event.timestamp).toLocaleString()}
</p>
{/* Document Links */}
{documentUrl && (
<div className="flex items-center gap-2 text-sm">
<ExternalLink className="h-4 w-4" aria-hidden="true" />
<a href={documentUrl} className="text-primary hover:underline">
{documentUrl}
</a>
</div>
)}
{/* Raw Payload (for debugging) */}
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
View raw payload
</summary>
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted p-2">
{JSON.stringify(event.payload, null, 2)}
</pre>
</details>
</div>
);
})()}
{/* Progress */}
{progress !== undefined && (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span>Progress</span>
<span>{progress}%</span>
</div>
<div className="h-2 rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary"
style={{ width: `${progress}%` }}
/>
</div>
</div>
)}
{/* Timestamp */}
<p className="text-xs text-muted-foreground">
{new Date(event.timestamp).toLocaleString()}
</p>
{/* Raw Payload (for debugging) */}
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
View raw payload
</summary>
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words rounded bg-muted p-2">
{JSON.stringify(event.payload, null, 2)}
</pre>
</details>
</div>
);
})()}
{/* Approval Actions */}
{isPendingApproval && (onApprove || onReject) && (
@@ -680,7 +763,12 @@ function EventItem({
</Button>
)}
{onReject && (
<Button variant="outline" size="sm" onClick={handleReject} data-testid="reject-button">
<Button
variant="outline"
size="sm"
onClick={handleReject}
data-testid="reject-button"
>
<XCircle className="mr-2 h-4 w-4" />
Reject
</Button>
@@ -712,7 +800,10 @@ function LoadingSkeleton() {
function EmptyState({ hasFilters }: { hasFilters: boolean }) {
return (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground" data-testid="empty-state">
<div
className="flex flex-col items-center justify-center py-12 text-muted-foreground"
data-testid="empty-state"
>
<Activity className="h-12 w-12 mb-4" aria-hidden="true" />
<h3 className="font-semibold">No activity found</h3>
<p className="text-sm">
@@ -894,7 +985,10 @@ export function ActivityFeed({
) : (
<div className="space-y-6">
{groupedEvents.map((group) => (
<div key={group.label} data-testid={`event-group-${group.label.toLowerCase().replace(' ', '-')}`}>
<div
key={group.label}
data-testid={`event-group-${group.label.toLowerCase().replace(' ', '-')}`}
>
<div className="mb-3 flex items-center gap-2">
<h3 className="text-sm font-medium text-muted-foreground">{group.label}</h3>
<Badge variant="secondary" className="text-xs">

View File

@@ -57,13 +57,19 @@ interface AgentTypeDetailProps {
function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
if (isActive) {
return (
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200" variant="outline">
<Badge
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
variant="outline"
>
Active
</Badge>
);
}
return (
<Badge className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200" variant="outline">
<Badge
className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
variant="outline"
>
Inactive
</Badge>
);
@@ -139,9 +145,7 @@ export function AgentTypeDetail({
<div className="py-12 text-center">
<AlertTriangle className="mx-auto h-12 w-12 text-muted-foreground" />
<h3 className="mt-4 font-semibold">Agent type not found</h3>
<p className="text-muted-foreground">
The requested agent type could not be found
</p>
<p className="text-muted-foreground">The requested agent type could not be found</p>
<Button onClick={onBack} variant="outline" className="mt-4">
<ArrowLeft className="mr-2 h-4 w-4" />
Go Back
@@ -265,9 +269,7 @@ export function AgentTypeDetail({
<div
key={server.id}
className={`flex items-center justify-between rounded-lg border p-3 ${
isEnabled
? 'border-primary/20 bg-primary/5'
: 'border-muted bg-muted/50'
isEnabled ? 'border-primary/20 bg-primary/5' : 'border-muted bg-muted/50'
}`}
>
<div className="flex items-center gap-3">
@@ -284,9 +286,7 @@ export function AgentTypeDetail({
</div>
<div>
<p className="font-medium">{server.name}</p>
<p className="text-xs text-muted-foreground">
{server.description}
</p>
<p className="text-xs text-muted-foreground">{server.description}</p>
</div>
</div>
<Badge variant={isEnabled ? 'default' : 'secondary'}>
@@ -313,9 +313,7 @@ export function AgentTypeDetail({
<CardContent className="space-y-4">
<div>
<p className="text-sm text-muted-foreground">Primary Model</p>
<p className="font-medium">
{getModelDisplayName(agentType.primary_model)}
</p>
<p className="font-medium">{getModelDisplayName(agentType.primary_model)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Failover Model</p>
@@ -355,9 +353,7 @@ export function AgentTypeDetail({
</CardHeader>
<CardContent>
<div className="text-center">
<p className="text-4xl font-bold text-primary">
{agentType.instance_count}
</p>
<p className="text-4xl font-bold text-primary">{agentType.instance_count}</p>
<p className="text-sm text-muted-foreground">Active instances</p>
</div>
<Button variant="outline" className="mt-4 w-full" size="sm" disabled>

View File

@@ -26,16 +26,7 @@ import {
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import {
FileText,
Cpu,
Shield,
MessageSquare,
Sliders,
Save,
ArrowLeft,
X,
} from 'lucide-react';
import { FileText, Cpu, Shield, MessageSquare, Sliders, Save, ArrowLeft, X } from 'lucide-react';
import {
agentTypeCreateSchema,
type AgentTypeCreateFormValues,
@@ -151,9 +142,7 @@ export function AgentTypeForm({
{isEditing ? 'Edit Agent Type' : 'Create Agent Type'}
</h1>
<p className="text-muted-foreground">
{isEditing
? 'Modify agent type configuration'
: 'Define a new agent type template'}
{isEditing ? 'Modify agent type configuration' : 'Define a new agent type template'}
</p>
</div>
<div className="flex gap-2">
@@ -281,9 +270,7 @@ export function AgentTypeForm({
<div className="space-y-2">
<Label>Expertise Areas</Label>
<p className="text-sm text-muted-foreground">
Add skills and areas of expertise
</p>
<p className="text-sm text-muted-foreground">Add skills and areas of expertise</p>
<div className="flex gap-2">
<Input
placeholder="e.g., System Design"
@@ -325,9 +312,7 @@ export function AgentTypeForm({
<Card>
<CardHeader>
<CardTitle>Model Selection</CardTitle>
<CardDescription>
Choose the AI models that power this agent type
</CardDescription>
<CardDescription>Choose the AI models that power this agent type</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-4 md:grid-cols-2">
@@ -358,9 +343,7 @@ export function AgentTypeForm({
{errors.primary_model.message}
</p>
)}
<p className="text-xs text-muted-foreground">
Main model used for this agent
</p>
<p className="text-xs text-muted-foreground">Main model used for this agent</p>
</div>
<div className="space-y-2">
<Label htmlFor="fallback_model">Fallover Model</Label>
@@ -420,9 +403,7 @@ export function AgentTypeForm({
/>
)}
/>
<p className="text-xs text-muted-foreground">
0 = deterministic, 2 = creative
</p>
<p className="text-xs text-muted-foreground">0 = deterministic, 2 = creative</p>
</div>
<div className="space-y-2">
<Label htmlFor="max_tokens">Max Tokens</Label>
@@ -472,9 +453,7 @@ export function AgentTypeForm({
<Card>
<CardHeader>
<CardTitle>MCP Server Permissions</CardTitle>
<CardDescription>
Configure which MCP servers this agent can access
</CardDescription>
<CardDescription>Configure which MCP servers this agent can access</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{AVAILABLE_MCP_SERVERS.map((server) => (
@@ -508,8 +487,8 @@ export function AgentTypeForm({
<CardHeader>
<CardTitle>Personality Prompt</CardTitle>
<CardDescription>
Define the agent&apos;s personality, behavior, and communication style. This
prompt shapes how the agent approaches tasks and interacts.
Define the agent&apos;s personality, behavior, and communication style. This prompt
shapes how the agent approaches tasks and interacts.
</CardDescription>
</CardHeader>
<CardContent>
@@ -535,9 +514,7 @@ export function AgentTypeForm({
</p>
)}
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>
Character count: {watch('personality_prompt')?.length || 0}
</span>
<span>Character count: {watch('personality_prompt')?.length || 0}</span>
<Separator orientation="vertical" className="h-4" />
<span className="text-xs">
Tip: Be specific about expertise, communication style, and decision-making

View File

@@ -41,13 +41,19 @@ interface AgentTypeListProps {
function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
if (isActive) {
return (
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200" variant="outline">
<Badge
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
variant="outline"
>
Active
</Badge>
);
}
return (
<Badge className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200" variant="outline">
<Badge
className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
variant="outline"
>
Inactive
</Badge>
);

View File

@@ -12,13 +12,7 @@
import { Component, type ReactNode } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
// ============================================================================
// Types
@@ -59,25 +53,17 @@ function DefaultFallback({ error, onReset, showReset }: DefaultFallbackProps) {
Something went wrong
</CardTitle>
<CardDescription>
An unexpected error occurred. Please try again or contact support if
the problem persists.
An unexpected error occurred. Please try again or contact support if the problem persists.
</CardDescription>
</CardHeader>
<CardContent>
{error && (
<div className="mb-4 rounded-md bg-muted p-3">
<p className="font-mono text-sm text-muted-foreground">
{error.message}
</p>
<p className="font-mono text-sm text-muted-foreground">{error.message}</p>
</div>
)}
{showReset && (
<Button
variant="outline"
size="sm"
onClick={onReset}
className="gap-2"
>
<Button variant="outline" size="sm" onClick={onReset} className="gap-2">
<RefreshCw className="h-4 w-4" aria-hidden="true" />
Try again
</Button>
@@ -108,10 +94,7 @@ function DefaultFallback({ error, onReset, showReset }: DefaultFallbackProps) {
* </ErrorBoundary>
* ```
*/
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
@@ -142,13 +125,7 @@ export class ErrorBoundary extends Component<
return fallback;
}
return (
<DefaultFallback
error={error}
onReset={this.handleReset}
showReset={showReset}
/>
);
return <DefaultFallback error={error} onReset={this.handleReset} showReset={showReset} />;
}
return children;

View File

@@ -153,7 +153,8 @@ export function ConnectionStatus({
className={cn(
'flex flex-col gap-3 rounded-lg border p-4',
state === 'error' && 'border-destructive bg-destructive/5',
state === 'connected' && 'border-green-200 bg-green-50 dark:border-green-900 dark:bg-green-950',
state === 'connected' &&
'border-green-200 bg-green-50 dark:border-green-900 dark:bg-green-950',
className
)}
role="status"
@@ -199,11 +200,7 @@ export function ConnectionStatus({
{showErrorDetails && error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm">
<p className="font-medium text-destructive">Error: {error.message}</p>
{error.code && (
<p className="mt-1 text-muted-foreground">
Code: {error.code}
</p>
)}
{error.code && <p className="mt-1 text-muted-foreground">Code: {error.code}</p>}
<p className="mt-1 text-xs text-muted-foreground">
{new Date(error.timestamp).toLocaleTimeString()}
</p>

View File

@@ -250,17 +250,11 @@ function getEventSummary(event: ProjectEvent): string {
? `Assigned to ${payload.assignee_name}`
: 'Issue assignment changed';
case EventType.ISSUE_CLOSED:
return payload.resolution
? `Closed: ${payload.resolution}`
: 'Issue closed';
return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed';
case EventType.SPRINT_STARTED:
return payload.sprint_name
? `Sprint "${payload.sprint_name}" started`
: 'Sprint started';
return payload.sprint_name ? `Sprint "${payload.sprint_name}" started` : 'Sprint started';
case EventType.SPRINT_COMPLETED:
return payload.sprint_name
? `Sprint "${payload.sprint_name}" completed`
: 'Sprint completed';
return payload.sprint_name ? `Sprint "${payload.sprint_name}" completed` : 'Sprint completed';
case EventType.APPROVAL_REQUESTED:
return String(payload.description || 'Approval requested');
case EventType.APPROVAL_GRANTED:
@@ -278,9 +272,7 @@ function getEventSummary(event: ProjectEvent): string {
? `Completed in ${payload.duration_seconds}s`
: 'Workflow completed';
case EventType.WORKFLOW_FAILED:
return payload.error_message
? String(payload.error_message)
: 'Workflow failed';
return payload.error_message ? String(payload.error_message) : 'Workflow failed';
default:
return event.type;
}

View File

@@ -72,8 +72,8 @@ export function HeroSection({ onOpenDemoModal }: HeroSectionProps) {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
Opinionated, secure, and production-ready. Syndarix gives you the solid foundation
you need to stop configuring and start shipping.{' '}
Opinionated, secure, and production-ready. Syndarix gives you the solid foundation you
need to stop configuring and start shipping.{' '}
<span className="text-foreground font-medium">Start building features on day one.</span>
</motion.p>

View File

@@ -74,11 +74,7 @@ function generateBreadcrumbs(pathname: string): BreadcrumbItem[] {
return breadcrumbs;
}
export function AppBreadcrumbs({
items,
showHome = true,
className,
}: AppBreadcrumbsProps) {
export function AppBreadcrumbs({ items, showHome = true, className }: AppBreadcrumbsProps) {
const pathname = usePathname();
// Use provided items or generate from pathname

View File

@@ -49,11 +49,7 @@ export function AppHeader({
{/* Left side - Logo and Project Switcher */}
<div className="flex items-center gap-4">
{/* Logo - visible on mobile, hidden on desktop when sidebar is visible */}
<Link
href="/"
className="flex items-center gap-2 lg:hidden"
aria-label="Syndarix home"
>
<Link href="/" className="flex items-center gap-2 lg:hidden" aria-label="Syndarix home">
<Image
src="/logo-icon.svg"
alt=""

View File

@@ -73,11 +73,7 @@ export function AppLayout({
{!hideBreadcrumbs && <AppBreadcrumbs items={breadcrumbs} />}
{/* Main content */}
<main
className={cn('flex-1', className)}
id="main-content"
tabIndex={-1}
>
<main className={cn('flex-1', className)} id="main-content" tabIndex={-1}>
{children}
</main>
</div>
@@ -110,11 +106,7 @@ const maxWidthClasses: Record<string, string> = {
full: 'max-w-full',
};
export function PageContainer({
children,
maxWidth = '6xl',
className,
}: PageContainerProps) {
export function PageContainer({ children, maxWidth = '6xl', className }: PageContainerProps) {
return (
<div
className={cn(
@@ -144,12 +136,7 @@ interface PageHeaderProps {
className?: string;
}
export function PageHeader({
title,
description,
actions,
className,
}: PageHeaderProps) {
export function PageHeader({ title, description, actions, className }: PageHeaderProps) {
return (
<div
className={cn(
@@ -160,9 +147,7 @@ export function PageHeader({
>
<div className="space-y-1">
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{title}</h1>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
{description && <p className="text-muted-foreground">{description}</p>}
</div>
{actions && <div className="flex items-center gap-2">{actions}</div>}
</div>

View File

@@ -98,9 +98,7 @@ export function ProjectSwitcher({
className={cn('gap-2 min-w-[160px] justify-between', className)}
data-testid="project-switcher-trigger"
aria-label={
currentProject
? `Switch project, current: ${currentProject.name}`
: 'Select project'
currentProject ? `Switch project, current: ${currentProject.name}` : 'Select project'
}
>
<div className="flex items-center gap-2">
@@ -112,11 +110,7 @@ export function ProjectSwitcher({
<ChevronDown className="h-4 w-4 opacity-50" aria-hidden="true" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="w-[200px]"
data-testid="project-switcher-menu"
>
<DropdownMenuContent align="start" className="w-[200px]" data-testid="project-switcher-menu">
<DropdownMenuLabel>Projects</DropdownMenuLabel>
<DropdownMenuSeparator />
{projects.map((project) => (

View File

@@ -11,13 +11,7 @@ import { Link } from '@/lib/i18n/routing';
import { usePathname } from '@/lib/i18n/routing';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import {
FolderKanban,
Bot,
@@ -113,9 +107,7 @@ function NavLink({ item, collapsed, basePath = '' }: NavLinkProps) {
const pathname = usePathname();
const href = basePath ? `${basePath}${item.href}` : item.href;
const isActive = item.exact
? pathname === href
: pathname.startsWith(href);
const isActive = item.exact ? pathname === href : pathname.startsWith(href);
const Icon = item.icon;
@@ -155,9 +147,7 @@ function SidebarContent({
<div className="flex h-full flex-col">
{/* Sidebar Header */}
<div className="flex h-14 items-center justify-between border-b px-4">
{!collapsed && (
<span className="text-lg font-semibold text-foreground">Navigation</span>
)}
{!collapsed && <span className="text-lg font-semibold text-foreground">Navigation</span>}
<Button
variant="ghost"
size="icon"
@@ -308,11 +298,7 @@ export function Sidebar({ projectSlug, className }: SidebarProps) {
data-testid="sidebar"
aria-label="Main navigation"
>
<SidebarContent
collapsed={collapsed}
projectSlug={projectSlug}
onToggle={handleToggle}
/>
<SidebarContent collapsed={collapsed} projectSlug={projectSlug} onToggle={handleToggle} />
</aside>
</>
);

View File

@@ -20,14 +20,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import {
User,
LogOut,
Shield,
Lock,
Monitor,
UserCog,
} from 'lucide-react';
import { User, LogOut, Shield, Lock, Monitor, UserCog } from 'lucide-react';
import { cn } from '@/lib/utils';
interface UserMenuProps {
@@ -76,20 +69,14 @@ export function UserMenu({ className }: UserMenuProps) {
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56"
align="end"
data-testid="user-menu-content"
>
<DropdownMenuContent className="w-56" align="end" data-testid="user-menu-content">
{/* User info header */}
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{user.first_name} {user.last_name}
</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email}
</p>
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
@@ -143,11 +130,7 @@ export function UserMenu({ className }: UserMenuProps) {
<>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
href="/admin"
className="cursor-pointer"
data-testid="user-menu-admin"
>
<Link href="/admin" className="cursor-pointer" data-testid="user-menu-admin">
<Shield className="mr-2 h-4 w-4" aria-hidden="true" />
{t('adminPanel')}
</Link>

View File

@@ -9,13 +9,7 @@
import { Bot, MoreVertical } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
@@ -228,11 +222,7 @@ export function AgentPanel({
) : (
<div className="space-y-3">
{agents.map((agent) => (
<AgentListItem
key={agent.id}
agent={agent}
onAction={onAgentAction}
/>
<AgentListItem key={agent.id} agent={agent} onAction={onAgentAction} />
))}
</div>
)}

View File

@@ -60,16 +60,10 @@ export function AgentStatusIndicator({
aria-label={`Status: ${config.label}`}
>
<span
className={cn(
'inline-block rounded-full',
sizeClasses[size],
config.color
)}
className={cn('inline-block rounded-full', sizeClasses[size], config.color)}
aria-hidden="true"
/>
{showLabel && (
<span className="text-xs text-muted-foreground">{config.label}</span>
)}
{showLabel && <span className="text-xs text-muted-foreground">{config.label}</span>}
</span>
);
}

View File

@@ -109,15 +109,7 @@ export function BurndownChart({
{data.map((d, i) => {
const x = padding.left + (i / (data.length - 1)) * innerWidth;
const y = padding.top + innerHeight - (d.remaining / maxPoints) * innerHeight;
return (
<circle
key={i}
cx={x}
cy={y}
r="2"
className="fill-primary"
/>
);
return <circle key={i} cx={x} cy={y} r="2" className="fill-primary" />;
})}
</svg>

View File

@@ -6,22 +6,10 @@
'use client';
import {
GitBranch,
CircleDot,
PlayCircle,
Clock,
AlertCircle,
CheckCircle2,
} from 'lucide-react';
import { GitBranch, CircleDot, PlayCircle, Clock, AlertCircle, CheckCircle2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Skeleton } from '@/components/ui/skeleton';
import type { IssueCountSummary } from './types';
@@ -141,12 +129,7 @@ export function IssueSummary({
</CardHeader>
<CardContent>
<div className="space-y-3" role="list" aria-label="Issue counts by status">
<StatusRow
icon={CircleDot}
iconColor="text-blue-500"
label="Open"
count={summary.open}
/>
<StatusRow icon={CircleDot} iconColor="text-blue-500" label="Open" count={summary.open} />
<StatusRow
icon={PlayCircle}
iconColor="text-yellow-500"
@@ -177,12 +160,7 @@ export function IssueSummary({
{onViewAllIssues && (
<div className="pt-2">
<Button
variant="outline"
className="w-full"
size="sm"
onClick={onViewAllIssues}
>
<Button variant="outline" className="w-full" size="sm" onClick={onViewAllIssues}>
View All Issues ({summary.total})
</Button>
</div>

View File

@@ -85,14 +85,12 @@ export function ProjectHeader({
}
const showPauseButton = canPause && project.status === 'active';
const showStartButton = canStart && project.status !== 'completed' && project.status !== 'archived';
const showStartButton =
canStart && project.status !== 'completed' && project.status !== 'archived';
return (
<div
className={cn(
'flex flex-col gap-4 md:flex-row md:items-start md:justify-between',
className
)}
className={cn('flex flex-col gap-4 md:flex-row md:items-start md:justify-between', className)}
data-testid="project-header"
>
{/* Project Info */}
@@ -102,20 +100,13 @@ export function ProjectHeader({
<ProjectStatusBadge status={project.status} />
<AutonomyBadge level={project.autonomy_level} />
</div>
{project.description && (
<p className="text-muted-foreground">{project.description}</p>
)}
{project.description && <p className="text-muted-foreground">{project.description}</p>}
</div>
{/* Quick Actions */}
<div className="flex flex-wrap gap-2">
{onSettings && (
<Button
variant="ghost"
size="icon"
onClick={onSettings}
aria-label="Project settings"
>
<Button variant="ghost" size="icon" onClick={onSettings} aria-label="Project settings">
<Settings className="h-4 w-4" />
</Button>
)}

View File

@@ -19,12 +19,7 @@ import {
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import type { ActivityItem } from './types';
@@ -104,9 +99,7 @@ function ActivityItemRow({ activity, onActionClick }: ActivityItemRowProps) {
</div>
<div className="min-w-0 flex-1">
<p className="text-sm">
{activity.agent && (
<span className="font-medium">{activity.agent}</span>
)}{' '}
{activity.agent && <span className="font-medium">{activity.agent}</span>}{' '}
<span className="text-muted-foreground">{activity.message}</span>
</p>
<p className="text-xs text-muted-foreground">{timestamp}</p>

View File

@@ -9,13 +9,7 @@
import { TrendingUp, Calendar } from 'lucide-react';
import { format } from 'date-fns';
import { cn } from '@/lib/utils';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Select,
SelectContent,
@@ -188,10 +182,7 @@ export function SprintProgress({
</div>
{availableSprints.length > 1 && onSprintChange && (
<Select
value={selectedSprintId || sprint.id}
onValueChange={onSprintChange}
>
<Select value={selectedSprintId || sprint.id} onValueChange={onSprintChange}>
<SelectTrigger className="w-32" aria-label="Select sprint">
<SelectValue />
</SelectTrigger>
@@ -231,16 +222,8 @@ export function SprintProgress({
label="In Progress"
colorClass="text-blue-600"
/>
<StatCard
value={sprint.blocked_issues}
label="Blocked"
colorClass="text-red-600"
/>
<StatCard
value={sprint.todo_issues}
label="To Do"
colorClass="text-gray-600"
/>
<StatCard value={sprint.blocked_issues} label="Blocked" colorClass="text-red-600" />
<StatCard value={sprint.todo_issues} label="To Do" colorClass="text-gray-600" />
</div>
{/* Burndown Chart */}

View File

@@ -81,9 +81,7 @@ export function AutonomyBadge({ level, showDescription = false, className }: Aut
<Badge variant="secondary" className={cn('gap-1', className)} title={config.description}>
<CircleDot className="h-3 w-3" aria-hidden="true" />
{config.label}
{showDescription && (
<span className="text-muted-foreground"> - {config.description}</span>
)}
{showDescription && <span className="text-muted-foreground"> - {config.description}</span>}
</Badge>
);
}

View File

@@ -123,7 +123,13 @@ export interface IssueCountSummary {
export interface ActivityItem {
id: string;
type: 'agent_message' | 'issue_update' | 'agent_status' | 'approval_request' | 'sprint_event' | 'system';
type:
| 'agent_message'
| 'issue_update'
| 'agent_status'
| 'approval_request'
| 'sprint_event'
| 'system';
agent?: string;
message: string;
timestamp: string;

View File

@@ -73,16 +73,13 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) {
mutationFn: async (projectData: ProjectCreateData): Promise<ProjectResponse> => {
// Call the projects API endpoint
// Note: The API client already handles authentication via interceptors
const response = await apiClient.instance.post<ProjectResponse>(
'/api/v1/projects',
{
name: projectData.name,
slug: projectData.slug,
description: projectData.description,
autonomy_level: projectData.autonomy_level,
settings: projectData.settings,
}
);
const response = await apiClient.instance.post<ProjectResponse>('/api/v1/projects', {
name: projectData.name,
slug: projectData.slug,
description: projectData.description,
autonomy_level: projectData.autonomy_level,
settings: projectData.settings,
});
return response.data;
},
@@ -123,7 +120,10 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) {
<Card className="text-center">
<CardContent className="space-y-6 p-8">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<CheckCircle2 className="h-8 w-8 text-green-600 dark:text-green-400" aria-hidden="true" />
<CheckCircle2
className="h-8 w-8 text-green-600 dark:text-green-400"
aria-hidden="true"
/>
</div>
<div>
<h2 className="text-2xl font-bold">Project Created Successfully!</h2>
@@ -192,10 +192,7 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) {
<ArrowRight className="ml-2 h-4 w-4" aria-hidden="true" />
</Button>
) : (
<Button
onClick={handleCreate}
disabled={createProjectMutation.isPending}
>
<Button onClick={handleCreate} disabled={createProjectMutation.isPending}>
{createProjectMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />

View File

@@ -29,7 +29,12 @@ export function StepIndicator({ currentStep, isScriptMode, className }: StepIndi
</span>
<span>{steps[displayStep - 1]}</span>
</div>
<div className="flex gap-1" role="progressbar" aria-valuenow={displayStep} aria-valuemax={totalSteps}>
<div
className="flex gap-1"
role="progressbar"
aria-valuenow={displayStep}
aria-valuemax={totalSteps}
>
{Array.from({ length: totalSteps }, (_, i) => (
<div
key={i}

View File

@@ -18,9 +18,4 @@ export type {
} from './types';
// Re-export constants
export {
complexityOptions,
clientModeOptions,
autonomyOptions,
WIZARD_STEPS,
} from './constants';
export { complexityOptions, clientModeOptions, autonomyOptions, WIZARD_STEPS } from './constants';

View File

@@ -11,13 +11,7 @@ import { Bot, User, MessageSquare, Sparkles } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';

View File

@@ -11,12 +11,7 @@
import { Check, AlertCircle } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { SelectableCard } from '../SelectableCard';
import { autonomyOptions } from '../constants';

View File

@@ -34,7 +34,11 @@ export function ClientModeStep({ state, updateState }: ClientModeStepProps) {
</p>
</div>
<div className="grid gap-6 md:grid-cols-2" role="radiogroup" aria-label="Client interaction mode options">
<div
className="grid gap-6 md:grid-cols-2"
role="radiogroup"
aria-label="Client interaction mode options"
>
{clientModeOptions.map((option) => {
const Icon = option.icon;
const isSelected = state.clientMode === option.id;

View File

@@ -39,7 +39,11 @@ export function ComplexityStep({ state, updateState }: ComplexityStepProps) {
)}
</div>
<div className="grid gap-4 md:grid-cols-2" role="radiogroup" aria-label="Project complexity options">
<div
className="grid gap-4 md:grid-cols-2"
role="radiogroup"
aria-label="Project complexity options"
>
{complexityOptions.map((option) => {
const Icon = option.icon;
const isSelected = state.complexity === option.id;

View File

@@ -8,12 +8,7 @@
import { CheckCircle2 } from 'lucide-react';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { complexityOptions, clientModeOptions, autonomyOptions } from '../constants';
import type { WizardState } from '../types';

View File

@@ -20,11 +20,7 @@ interface ActivityTimelineProps {
className?: string;
}
export function ActivityTimeline({
activities,
onAddComment,
className,
}: ActivityTimelineProps) {
export function ActivityTimeline({ activities, onAddComment, className }: ActivityTimelineProps) {
return (
<Card className={className}>
<CardHeader>
@@ -43,11 +39,7 @@ export function ActivityTimeline({
<CardContent>
<div className="space-y-6" role="list" aria-label="Issue activity">
{activities.map((item, index) => (
<div
key={item.id}
className="flex gap-4"
role="listitem"
>
<div key={item.id} className="flex gap-4" role="listitem">
<div className="relative flex flex-col items-center">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted">
{item.actor.type === 'agent' ? (
@@ -74,9 +66,7 @@ export function ActivityTimeline({
</div>
{activities.length === 0 && (
<div className="py-8 text-center text-muted-foreground">
No activity yet
</div>
<div className="py-8 text-center text-muted-foreground">No activity yet</div>
)}
</CardContent>
</Card>

View File

@@ -34,16 +34,11 @@ export function BulkActions({
return (
<div
className={cn(
'flex items-center gap-4 rounded-lg border bg-muted/50 p-3',
className
)}
className={cn('flex items-center gap-4 rounded-lg border bg-muted/50 p-3', className)}
role="toolbar"
aria-label="Bulk actions for selected issues"
>
<span className="text-sm font-medium">
{selectedCount} selected
</span>
<span className="text-sm font-medium">{selectedCount} selected</span>
<Separator orientation="vertical" className="h-6" />
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={onChangeStatus}>

View File

@@ -44,9 +44,7 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
</div>
<div>
<p className="font-medium">{issue.assignee.name}</p>
<p className="text-xs text-muted-foreground capitalize">
{issue.assignee.type}
</p>
<p className="text-xs text-muted-foreground capitalize">{issue.assignee.type}</p>
</div>
</div>
) : (
@@ -92,9 +90,7 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
{issue.due_date && (
<div>
<p className="text-sm text-muted-foreground">Due Date</p>
<p className="font-medium">
{new Date(issue.due_date).toLocaleDateString()}
</p>
<p className="font-medium">{new Date(issue.due_date).toLocaleDateString()}</p>
</div>
)}
@@ -136,19 +132,13 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
<CardContent className="space-y-4">
{issue.branch && (
<div className="flex items-center gap-2">
<GitBranch
className="h-4 w-4 text-muted-foreground"
aria-hidden="true"
/>
<GitBranch className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
<span className="font-mono text-sm">{issue.branch}</span>
</div>
)}
{issue.pull_request && (
<div className="flex items-center gap-2">
<GitPullRequest
className="h-4 w-4 text-muted-foreground"
aria-hidden="true"
/>
<GitPullRequest className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
<span className="text-sm">{issue.pull_request}</span>
<Badge variant="outline" className="text-xs">
Open

View File

@@ -136,10 +136,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
<div className="grid gap-4 sm:grid-cols-4">
<div className="space-y-2">
<Label htmlFor="priority-filter">Priority</Label>
<Select
value={filters.priority || 'all'}
onValueChange={handlePriorityChange}
>
<Select value={filters.priority || 'all'} onValueChange={handlePriorityChange}>
<SelectTrigger id="priority-filter">
<SelectValue placeholder="All" />
</SelectTrigger>
@@ -172,10 +169,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
</div>
<div className="space-y-2">
<Label htmlFor="assignee-filter">Assignee</Label>
<Select
value={filters.assignee || 'all'}
onValueChange={handleAssigneeChange}
>
<Select value={filters.assignee || 'all'} onValueChange={handleAssigneeChange}>
<SelectTrigger id="assignee-filter">
<SelectValue placeholder="All" />
</SelectTrigger>

View File

@@ -8,14 +8,7 @@
* @module features/issues/components/StatusBadge
*/
import {
CircleDot,
PlayCircle,
Clock,
AlertCircle,
CheckCircle2,
XCircle,
} from 'lucide-react';
import { CircleDot, PlayCircle, Clock, AlertCircle, CheckCircle2, XCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { IssueStatus } from '../types';
import { STATUS_CONFIG } from '../constants';
@@ -42,9 +35,7 @@ export function StatusBadge({ status, className, showLabel = true }: StatusBadge
return (
<div className={cn('flex items-center gap-1.5', config.color, className)}>
<Icon className="h-4 w-4" aria-hidden="true" />
{showLabel && (
<span className="text-sm font-medium">{config.label}</span>
)}
{showLabel && <span className="text-sm font-medium">{config.label}</span>}
<span className="sr-only">{config.label}</span>
</div>
);

View File

@@ -8,14 +8,7 @@
* @module features/issues/components/StatusWorkflow
*/
import {
CircleDot,
PlayCircle,
Clock,
AlertCircle,
CheckCircle2,
XCircle,
} from 'lucide-react';
import { CircleDot, PlayCircle, Clock, AlertCircle, CheckCircle2, XCircle } from 'lucide-react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import type { IssueStatus } from '../types';
@@ -63,18 +56,14 @@ export function StatusWorkflow({
disabled={disabled}
className={cn(
'flex w-full items-center gap-2 rounded-lg p-2 text-left transition-colors',
isActive
? 'bg-primary/10 text-primary'
: 'hover:bg-muted',
isActive ? 'bg-primary/10 text-primary' : 'hover:bg-muted',
disabled && 'cursor-not-allowed opacity-50'
)}
onClick={() => !disabled && onStatusChange(status)}
>
<Icon className={cn('h-4 w-4', config.color)} aria-hidden="true" />
<span className="text-sm">{config.label}</span>
{isActive && (
<CheckCircle2 className="ml-auto h-4 w-4" aria-hidden="true" />
)}
{isActive && <CheckCircle2 className="ml-auto h-4 w-4" aria-hidden="true" />}
</button>
);
})}

View File

@@ -18,8 +18,7 @@ export const mockIssues: IssueSummary[] = [
number: 42,
type: 'story',
title: 'Implement user authentication flow',
description:
'Create complete authentication flow with login, register, and password reset.',
description: 'Create complete authentication flow with login, register, and password reset.',
status: 'in_progress',
priority: 'high',
labels: ['feature', 'auth', 'backend'],

View File

@@ -44,12 +44,7 @@ const DEFAULT_PAGE_LIMIT = 20;
export function useAgentTypes(params: AgentTypeListParams = {}) {
const { user } = useAuth();
const {
page = 1,
limit = DEFAULT_PAGE_LIMIT,
is_active = true,
search,
} = params;
const { page = 1, limit = DEFAULT_PAGE_LIMIT, is_active = true, search } = params;
return useQuery({
queryKey: agentTypeKeys.list({ page, limit, is_active, search }),
@@ -152,10 +147,7 @@ export function useUpdateAgentType() {
},
onSuccess: (updatedAgentType) => {
// Update the cache for this specific agent type
queryClient.setQueryData(
agentTypeKeys.detail(updatedAgentType.id),
updatedAgentType
);
queryClient.setQueryData(agentTypeKeys.detail(updatedAgentType.id), updatedAgentType);
// Invalidate lists to reflect changes
queryClient.invalidateQueries({ queryKey: agentTypeKeys.lists() });
},

View File

@@ -5,4 +5,8 @@
*/
export { useDebounce } from './useDebounce';
export { useProjectEvents, type UseProjectEventsOptions, type UseProjectEventsResult } from './useProjectEvents';
export {
useProjectEvents,
type UseProjectEventsOptions,
type UseProjectEventsResult,
} from './useProjectEvents';

View File

@@ -385,7 +385,16 @@ export function useProjectEvents(
mountedRef.current = false;
cleanup();
};
}, [autoConnect, isAuthenticated, accessToken, projectId, connectionState, connect, disconnect, cleanup]);
}, [
autoConnect,
isAuthenticated,
accessToken,
projectId,
connectionState,
connect,
disconnect,
cleanup,
]);
return {
events,

View File

@@ -53,10 +53,7 @@ const modelParamsSchema = z.object({
* Schema for agent type form fields
*/
export const agentTypeFormSchema = z.object({
name: z
.string()
.min(1, 'Name is required')
.max(255, 'Name must be less than 255 characters'),
name: z.string().min(1, 'Name is required').max(255, 'Name must be less than 255 characters'),
slug: z
.string()

View File

@@ -245,7 +245,10 @@ describe('HomePage', () => {
const githubLinks = screen.getAllByRole('link', { name: /GitHub/i });
expect(githubLinks.length).toBeGreaterThan(0);
// Syndarix uses Gitea for version control
expect(githubLinks[0]).toHaveAttribute('href', expect.stringContaining('gitea.pragmazest.com'));
expect(githubLinks[0]).toHaveAttribute(
'href',
expect.stringContaining('gitea.pragmazest.com')
);
});
});

View File

@@ -356,9 +356,7 @@ describe('ActivityFeed', () => {
await user.click(within(eventItem).getByTestId('approve-button'));
expect(onApprove).toHaveBeenCalledTimes(1);
expect(onApprove).toHaveBeenCalledWith(
expect.objectContaining({ id: 'event-001' })
);
expect(onApprove).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
});
it('calls onReject when reject button clicked', async () => {
@@ -370,9 +368,7 @@ describe('ActivityFeed', () => {
await user.click(within(eventItem).getByTestId('reject-button'));
expect(onReject).toHaveBeenCalledTimes(1);
expect(onReject).toHaveBeenCalledWith(
expect.objectContaining({ id: 'event-001' })
);
expect(onReject).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
});
it('shows pending count badge', () => {
@@ -440,9 +436,7 @@ describe('ActivityFeed', () => {
await user.click(screen.getByTestId('event-item-event-001'));
expect(onEventClick).toHaveBeenCalledTimes(1);
expect(onEventClick).toHaveBeenCalledWith(
expect.objectContaining({ id: 'event-001' })
);
expect(onEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
});
});
@@ -459,7 +453,9 @@ describe('ActivityFeed', () => {
describe('Accessibility', () => {
it('has proper ARIA labels for interactive elements', () => {
render(<ActivityFeed {...defaultProps} onReconnect={jest.fn()} connectionState="disconnected" />);
render(
<ActivityFeed {...defaultProps} onReconnect={jest.fn()} connectionState="disconnected" />
);
expect(screen.getByLabelText('Reconnect')).toBeInTheDocument();
});

View File

@@ -160,9 +160,7 @@ describe('AgentTypeDetail', () => {
it('shows not found state when agentType is null', () => {
render(<AgentTypeDetail {...defaultProps} agentType={null} isLoading={false} />);
expect(screen.getByText('Agent type not found')).toBeInTheDocument();
expect(
screen.getByText('The requested agent type could not be found')
).toBeInTheDocument();
expect(screen.getByText('The requested agent type could not be found')).toBeInTheDocument();
});
it('shows danger zone with deactivate button', () => {
@@ -191,9 +189,7 @@ describe('AgentTypeDetail', () => {
});
it('applies custom className', () => {
const { container } = render(
<AgentTypeDetail {...defaultProps} className="custom-class" />
);
const { container } = render(<AgentTypeDetail {...defaultProps} className="custom-class" />);
expect(container.firstChild).toHaveClass('custom-class');
});
@@ -205,18 +201,13 @@ describe('AgentTypeDetail', () => {
});
it('shows no expertise message when expertise is empty', () => {
render(
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, expertise: [] }} />
);
render(<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, expertise: [] }} />);
expect(screen.getByText('No expertise areas defined')).toBeInTheDocument();
});
it('shows "None configured" when no fallback model', () => {
render(
<AgentTypeDetail
{...defaultProps}
agentType={{ ...mockAgentType, fallback_models: [] }}
/>
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, fallback_models: [] }} />
);
expect(screen.getByText('None configured')).toBeInTheDocument();
});

View File

@@ -36,9 +36,7 @@ describe('AgentTypeForm', () => {
it('renders create form title', () => {
render(<AgentTypeForm {...defaultProps} />);
expect(screen.getByText('Create Agent Type')).toBeInTheDocument();
expect(
screen.getByText('Define a new agent type template')
).toBeInTheDocument();
expect(screen.getByText('Define a new agent type template')).toBeInTheDocument();
});
it('renders all tabs', () => {
@@ -233,9 +231,7 @@ describe('AgentTypeForm', () => {
});
it('applies custom className', () => {
const { container } = render(
<AgentTypeForm {...defaultProps} className="custom-class" />
);
const { container } = render(<AgentTypeForm {...defaultProps} className="custom-class" />);
expect(container.querySelector('form')).toHaveClass('custom-class');
});
});

View File

@@ -143,15 +143,11 @@ describe('AgentTypeList', () => {
it('shows empty state when no agent types', () => {
render(<AgentTypeList {...defaultProps} agentTypes={[]} />);
expect(screen.getByText('No agent types found')).toBeInTheDocument();
expect(
screen.getByText('Create your first agent type to get started')
).toBeInTheDocument();
expect(screen.getByText('Create your first agent type to get started')).toBeInTheDocument();
});
it('shows filter hint in empty state when filters are applied', () => {
render(
<AgentTypeList {...defaultProps} agentTypes={[]} searchQuery="nonexistent" />
);
render(<AgentTypeList {...defaultProps} agentTypes={[]} searchQuery="nonexistent" />);
expect(screen.getByText('Try adjusting your search or filters')).toBeInTheDocument();
});
@@ -175,9 +171,7 @@ describe('AgentTypeList', () => {
});
it('applies custom className', () => {
const { container } = render(
<AgentTypeList {...defaultProps} className="custom-class" />
);
const { container } = render(<AgentTypeList {...defaultProps} className="custom-class" />);
expect(container.firstChild).toHaveClass('custom-class');
});
});

View File

@@ -182,9 +182,7 @@ describe('ConnectionStatus', () => {
describe('className prop', () => {
it('applies custom className', () => {
const { container } = render(
<ConnectionStatus state="connected" className="custom-class" />
);
const { container } = render(<ConnectionStatus state="connected" className="custom-class" />);
expect(container.querySelector('.custom-class')).toBeInTheDocument();
});

View File

@@ -71,7 +71,10 @@ describe('CTASection', () => {
);
const githubLink = screen.getByRole('link', { name: /get started on github/i });
expect(githubLink).toHaveAttribute('href', 'https://gitea.pragmazest.com/cardosofelipe/syndarix');
expect(githubLink).toHaveAttribute(
'href',
'https://gitea.pragmazest.com/cardosofelipe/syndarix'
);
expect(githubLink).toHaveAttribute('target', '_blank');
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
});

View File

@@ -221,7 +221,10 @@ describe('Header', () => {
const mobileGithubLink = githubLinks[1];
fireEvent.click(mobileGithubLink);
// Syndarix uses Gitea for version control
expect(mobileGithubLink).toHaveAttribute('href', expect.stringContaining('gitea.pragmazest.com'));
expect(mobileGithubLink).toHaveAttribute(
'href',
expect.stringContaining('gitea.pragmazest.com')
);
}
});

View File

@@ -100,7 +100,10 @@ describe('HeroSection', () => {
);
const githubLink = screen.getByRole('link', { name: /view on github/i });
expect(githubLink).toHaveAttribute('href', 'https://gitea.pragmazest.com/cardosofelipe/syndarix');
expect(githubLink).toHaveAttribute(
'href',
'https://gitea.pragmazest.com/cardosofelipe/syndarix'
);
expect(githubLink).toHaveAttribute('target', '_blank');
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
});

View File

@@ -123,12 +123,7 @@ describe('AppHeader', () => {
});
it('displays current project name', () => {
render(
<AppHeader
projects={mockProjects}
currentProject={mockProjects[0]}
/>
);
render(<AppHeader projects={mockProjects} currentProject={mockProjects[0]} />);
// Multiple instances may show the project name
expect(screen.getAllByText('Project One').length).toBeGreaterThan(0);
@@ -137,12 +132,7 @@ describe('AppHeader', () => {
it('calls onProjectChange when project is changed', async () => {
const mockOnChange = jest.fn();
render(
<AppHeader
projects={mockProjects}
onProjectChange={mockOnChange}
/>
);
render(<AppHeader projects={mockProjects} onProjectChange={mockOnChange} />);
// The actual test of project switching is in ProjectSwitcher.test.tsx
// Here we just verify the prop is passed by checking switcher exists

View File

@@ -145,9 +145,7 @@ describe('AppLayout', () => {
});
it('passes custom breadcrumbs to AppBreadcrumbs', () => {
const customBreadcrumbs = [
{ label: 'Custom', href: '/custom', current: true },
];
const customBreadcrumbs = [{ label: 'Custom', href: '/custom', current: true }];
render(
<AppLayout breadcrumbs={customBreadcrumbs}>
@@ -344,10 +342,7 @@ describe('PageHeader', () => {
it('renders actions when provided', () => {
render(
<PageHeader
title="Title"
actions={<button data-testid="action-button">Action</button>}
/>
<PageHeader title="Title" actions={<button data-testid="action-button">Action</button>} />
);
expect(screen.getByTestId('action-button')).toBeInTheDocument();

View File

@@ -45,12 +45,7 @@ describe('ProjectSwitcher', () => {
});
it('displays current project name', () => {
render(
<ProjectSwitcher
projects={mockProjects}
currentProject={mockProjects[0]}
/>
);
render(<ProjectSwitcher projects={mockProjects} currentProject={mockProjects[0]} />);
expect(screen.getByText('Project One')).toBeInTheDocument();
});
@@ -94,12 +89,7 @@ describe('ProjectSwitcher', () => {
it('shows current indicator on selected project', async () => {
const user = userEvent.setup();
render(
<ProjectSwitcher
projects={mockProjects}
currentProject={mockProjects[0]}
/>
);
render(<ProjectSwitcher projects={mockProjects} currentProject={mockProjects[0]} />);
const trigger = screen.getByTestId('project-switcher-trigger');
await user.click(trigger);
@@ -144,12 +134,7 @@ describe('ProjectSwitcher', () => {
const user = userEvent.setup();
const mockOnChange = jest.fn();
render(
<ProjectSwitcher
projects={mockProjects}
onProjectChange={mockOnChange}
/>
);
render(<ProjectSwitcher projects={mockProjects} onProjectChange={mockOnChange} />);
const trigger = screen.getByTestId('project-switcher-trigger');
await user.click(trigger);
@@ -189,18 +174,10 @@ describe('ProjectSwitcher', () => {
describe('Accessibility', () => {
it('has accessible label on trigger', () => {
render(
<ProjectSwitcher
projects={mockProjects}
currentProject={mockProjects[0]}
/>
);
render(<ProjectSwitcher projects={mockProjects} currentProject={mockProjects[0]} />);
const trigger = screen.getByTestId('project-switcher-trigger');
expect(trigger).toHaveAttribute(
'aria-label',
'Switch project, current: Project One'
);
expect(trigger).toHaveAttribute('aria-label', 'Switch project, current: Project One');
});
it('has accessible label when no current project', () => {
@@ -220,12 +197,7 @@ describe('ProjectSelect', () => {
describe('Rendering', () => {
it('renders select component', () => {
render(
<ProjectSelect
projects={mockProjects}
onValueChange={jest.fn()}
/>
);
render(<ProjectSelect projects={mockProjects} onValueChange={jest.fn()} />);
expect(screen.getByTestId('project-select')).toBeInTheDocument();
});
@@ -243,23 +215,14 @@ describe('ProjectSelect', () => {
});
it('has combobox role', () => {
render(
<ProjectSelect
projects={mockProjects}
onValueChange={jest.fn()}
/>
);
render(<ProjectSelect projects={mockProjects} onValueChange={jest.fn()} />);
expect(screen.getByRole('combobox')).toBeInTheDocument();
});
it('applies custom className', () => {
render(
<ProjectSelect
projects={mockProjects}
onValueChange={jest.fn()}
className="custom-class"
/>
<ProjectSelect projects={mockProjects} onValueChange={jest.fn()} className="custom-class" />
);
const select = screen.getByTestId('project-select');

View File

@@ -74,7 +74,12 @@ const mockUseProjectEventsDefault = {
events: [] as ProjectEvent[],
isConnected: true,
connectionState: 'connected' as ConnectionState,
error: null as { message: string; timestamp: string; code?: string; retryAttempt?: number } | null,
error: null as {
message: string;
timestamp: string;
code?: string;
retryAttempt?: number;
} | null,
retryCount: 0,
reconnect: mockReconnect,
disconnect: mockDisconnect,
@@ -389,11 +394,7 @@ describe('Event to Activity Conversion', () => {
});
it('handles system actor type', () => {
const event = createMockEvent(
EventType.SPRINT_STARTED,
{ sprint_name: 'Sprint 5' },
'system'
);
const event = createMockEvent(EventType.SPRINT_STARTED, { sprint_name: 'Sprint 5' }, 'system');
mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();

View File

@@ -46,13 +46,7 @@ describe('ProjectHeader', () => {
it('shows pause button when canPause is true and project is active', () => {
const onPauseProject = jest.fn();
render(
<ProjectHeader
project={mockProject}
canPause={true}
onPauseProject={onPauseProject}
/>
);
render(<ProjectHeader project={mockProject} canPause={true} onPauseProject={onPauseProject} />);
expect(screen.getByRole('button', { name: /pause project/i })).toBeInTheDocument();
});
@@ -64,13 +58,7 @@ describe('ProjectHeader', () => {
it('shows run sprint button when canStart is true', () => {
const onStartSprint = jest.fn();
render(
<ProjectHeader
project={mockProject}
canStart={true}
onStartSprint={onStartSprint}
/>
);
render(<ProjectHeader project={mockProject} canStart={true} onStartSprint={onStartSprint} />);
expect(screen.getByRole('button', { name: /run sprint/i })).toBeInTheDocument();
});
@@ -83,13 +71,7 @@ describe('ProjectHeader', () => {
it('calls onStartSprint when run sprint button is clicked', async () => {
const user = userEvent.setup();
const onStartSprint = jest.fn();
render(
<ProjectHeader
project={mockProject}
canStart={true}
onStartSprint={onStartSprint}
/>
);
render(<ProjectHeader project={mockProject} canStart={true} onStartSprint={onStartSprint} />);
await user.click(screen.getByRole('button', { name: /run sprint/i }));
expect(onStartSprint).toHaveBeenCalledTimes(1);
@@ -98,13 +80,7 @@ describe('ProjectHeader', () => {
it('calls onPauseProject when pause button is clicked', async () => {
const user = userEvent.setup();
const onPauseProject = jest.fn();
render(
<ProjectHeader
project={mockProject}
canPause={true}
onPauseProject={onPauseProject}
/>
);
render(<ProjectHeader project={mockProject} canPause={true} onPauseProject={onPauseProject} />);
await user.click(screen.getByRole('button', { name: /pause project/i }));
expect(onPauseProject).toHaveBeenCalledTimes(1);
@@ -113,12 +89,7 @@ describe('ProjectHeader', () => {
it('calls onCreateSprint when new sprint button is clicked', async () => {
const user = userEvent.setup();
const onCreateSprint = jest.fn();
render(
<ProjectHeader
project={mockProject}
onCreateSprint={onCreateSprint}
/>
);
render(<ProjectHeader project={mockProject} onCreateSprint={onCreateSprint} />);
await user.click(screen.getByRole('button', { name: /new sprint/i }));
expect(onCreateSprint).toHaveBeenCalledTimes(1);
@@ -127,12 +98,7 @@ describe('ProjectHeader', () => {
it('calls onSettings when settings button is clicked', async () => {
const user = userEvent.setup();
const onSettings = jest.fn();
render(
<ProjectHeader
project={mockProject}
onSettings={onSettings}
/>
);
render(<ProjectHeader project={mockProject} onSettings={onSettings} />);
await user.click(screen.getByRole('button', { name: /project settings/i }));
expect(onSettings).toHaveBeenCalledTimes(1);

View File

@@ -65,38 +65,20 @@ describe('RecentActivity', () => {
it('shows View All button when there are more activities than maxItems', () => {
const onViewAll = jest.fn();
render(
<RecentActivity
activities={mockActivities}
maxItems={2}
onViewAll={onViewAll}
/>
);
render(<RecentActivity activities={mockActivities} maxItems={2} onViewAll={onViewAll} />);
expect(screen.getByRole('button', { name: /view all/i })).toBeInTheDocument();
});
it('does not show View All button when all activities are shown', () => {
const onViewAll = jest.fn();
render(
<RecentActivity
activities={mockActivities}
maxItems={5}
onViewAll={onViewAll}
/>
);
render(<RecentActivity activities={mockActivities} maxItems={5} onViewAll={onViewAll} />);
expect(screen.queryByRole('button', { name: /view all/i })).not.toBeInTheDocument();
});
it('calls onViewAll when View All button is clicked', async () => {
const user = userEvent.setup();
const onViewAll = jest.fn();
render(
<RecentActivity
activities={mockActivities}
maxItems={2}
onViewAll={onViewAll}
/>
);
render(<RecentActivity activities={mockActivities} maxItems={2} onViewAll={onViewAll} />);
await user.click(screen.getByRole('button', { name: /view all/i }));
expect(onViewAll).toHaveBeenCalledTimes(1);
@@ -104,24 +86,14 @@ describe('RecentActivity', () => {
it('shows Review Request button for items requiring action', () => {
const onActionClick = jest.fn();
render(
<RecentActivity
activities={mockActivities}
onActionClick={onActionClick}
/>
);
render(<RecentActivity activities={mockActivities} onActionClick={onActionClick} />);
expect(screen.getByRole('button', { name: /review request/i })).toBeInTheDocument();
});
it('calls onActionClick when Review Request button is clicked', async () => {
const user = userEvent.setup();
const onActionClick = jest.fn();
render(
<RecentActivity
activities={mockActivities}
onActionClick={onActionClick}
/>
);
render(<RecentActivity activities={mockActivities} onActionClick={onActionClick} />);
await user.click(screen.getByRole('button', { name: /review request/i }));
expect(onActionClick).toHaveBeenCalledWith('act-003');

View File

@@ -23,9 +23,7 @@ describe('ProjectStatusBadge', () => {
});
it('applies custom className', () => {
const { container } = render(
<ProjectStatusBadge status="active" className="custom-class" />
);
const { container } = render(<ProjectStatusBadge status="active" className="custom-class" />);
expect(container.firstChild).toHaveClass('custom-class');
});
});
@@ -54,9 +52,7 @@ describe('AutonomyBadge', () => {
});
it('applies custom className', () => {
const { container } = render(
<AutonomyBadge level="milestone" className="custom-class" />
);
const { container } = render(<AutonomyBadge level="milestone" className="custom-class" />);
expect(container.firstChild).toHaveClass('custom-class');
});
});

View File

@@ -264,7 +264,9 @@ describe('ProjectWizard', () => {
await user.click(screen.getByRole('button', { name: /create project/i }));
await waitFor(() => {
expect(screen.getByRole('button', { name: /go to project dashboard/i })).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /go to project dashboard/i })
).toBeInTheDocument();
});
await user.click(screen.getByRole('button', { name: /go to project dashboard/i }));

View File

@@ -60,7 +60,9 @@ describe('AutonomyStep', () => {
it('has accessible radiogroup role', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByRole('radiogroup', { name: /autonomy level options/i })).toBeInTheDocument();
expect(
screen.getByRole('radiogroup', { name: /autonomy level options/i })
).toBeInTheDocument();
});
});
@@ -69,7 +71,9 @@ describe('AutonomyStep', () => {
const user = userEvent.setup();
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const fullControlOption = screen.getByRole('button', { name: /full control.*review every action/i });
const fullControlOption = screen.getByRole('button', {
name: /full control.*review every action/i,
});
await user.click(fullControlOption);
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'full_control' });
@@ -89,7 +93,9 @@ describe('AutonomyStep', () => {
const user = userEvent.setup();
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />);
const autonomousOption = screen.getByRole('button', { name: /autonomous.*only major decisions/i });
const autonomousOption = screen.getByRole('button', {
name: /autonomous.*only major decisions/i,
});
await user.click(autonomousOption);
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });
@@ -180,7 +186,7 @@ describe('AutonomyStep', () => {
autonomyOptions.forEach((option) => {
const button = screen.getByRole('button', {
name: new RegExp(`${option.label}.*${option.description}`, 'i')
name: new RegExp(`${option.label}.*${option.description}`, 'i'),
});
expect(button).toBeInTheDocument();
});
@@ -207,7 +213,9 @@ describe('AutonomyStep', () => {
};
render(<AutonomyStep state={stateWithFullControl} updateState={mockUpdateState} />);
const autonomousOption = screen.getByRole('button', { name: /autonomous.*only major decisions/i });
const autonomousOption = screen.getByRole('button', {
name: /autonomous.*only major decisions/i,
});
await user.click(autonomousOption);
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });

View File

@@ -59,7 +59,9 @@ describe('ClientModeStep', () => {
it('has accessible radiogroup role', () => {
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByRole('radiogroup', { name: /client interaction mode options/i })).toBeInTheDocument();
expect(
screen.getByRole('radiogroup', { name: /client interaction mode options/i })
).toBeInTheDocument();
});
});
@@ -76,7 +78,9 @@ describe('ClientModeStep', () => {
const user = userEvent.setup();
render(<ClientModeStep state={defaultState} updateState={mockUpdateState} />);
const technicalOption = screen.getByRole('button', { name: /technical mode.*detailed technical/i });
const technicalOption = screen.getByRole('button', {
name: /technical mode.*detailed technical/i,
});
await user.click(technicalOption);
expect(mockUpdateState).toHaveBeenCalledWith({ clientMode: 'technical' });
@@ -123,7 +127,7 @@ describe('ClientModeStep', () => {
clientModeOptions.forEach((option) => {
const button = screen.getByRole('button', {
name: new RegExp(`${option.label}.*${option.description}`, 'i')
name: new RegExp(`${option.label}.*${option.description}`, 'i'),
});
expect(button).toBeInTheDocument();
});
@@ -168,7 +172,9 @@ describe('ClientModeStep', () => {
};
render(<ClientModeStep state={stateWithTechnical} updateState={mockUpdateState} />);
const technicalOption = screen.getByRole('button', { name: /technical mode.*detailed technical/i });
const technicalOption = screen.getByRole('button', {
name: /technical mode.*detailed technical/i,
});
await user.click(technicalOption);
// Should still call updateState

View File

@@ -65,7 +65,9 @@ describe('ComplexityStep', () => {
it('has accessible radiogroup role', () => {
render(<ComplexityStep state={defaultState} updateState={mockUpdateState} />);
expect(screen.getByRole('radiogroup', { name: /project complexity options/i })).toBeInTheDocument();
expect(
screen.getByRole('radiogroup', { name: /project complexity options/i })
).toBeInTheDocument();
});
});
@@ -164,7 +166,7 @@ describe('ComplexityStep', () => {
complexityOptions.forEach((option) => {
const button = screen.getByRole('button', {
name: new RegExp(`${option.label}.*${option.description}`, 'i')
name: new RegExp(`${option.label}.*${option.description}`, 'i'),
});
expect(button).toBeInTheDocument();
});

View File

@@ -211,9 +211,7 @@ describe('IssueDetailPanel', () => {
it('renders labels without color property', () => {
const issueWithColorlessLabels: IssueDetail = {
...defaultIssue,
labels: [
{ id: 'lbl-1', name: 'colorless-label' },
],
labels: [{ id: 'lbl-1', name: 'colorless-label' }],
};
render(<IssueDetailPanel issue={issueWithColorlessLabels} />);
expect(screen.getByText('colorless-label')).toBeInTheDocument();

View File

@@ -14,9 +14,7 @@ describe('StatusWorkflow', () => {
});
it('renders all status options', () => {
render(
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
);
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
expect(screen.getByText('Open')).toBeInTheDocument();
expect(screen.getByText('In Progress')).toBeInTheDocument();
@@ -26,9 +24,7 @@ describe('StatusWorkflow', () => {
});
it('highlights current status', () => {
render(
<StatusWorkflow currentStatus="in_progress" onStatusChange={mockOnStatusChange} />
);
render(<StatusWorkflow currentStatus="in_progress" onStatusChange={mockOnStatusChange} />);
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
expect(inProgressButton).toHaveAttribute('aria-checked', 'true');
@@ -36,9 +32,7 @@ describe('StatusWorkflow', () => {
it('calls onStatusChange when status is clicked', async () => {
const user = userEvent.setup();
render(
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
);
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
await user.click(inProgressButton);
@@ -48,9 +42,7 @@ describe('StatusWorkflow', () => {
it('disables status buttons when disabled prop is true', async () => {
const user = userEvent.setup();
render(
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} disabled />
);
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} disabled />);
const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
expect(inProgressButton).toBeDisabled();
@@ -72,9 +64,7 @@ describe('StatusWorkflow', () => {
});
it('has proper radiogroup role', () => {
render(
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
);
render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
expect(screen.getByRole('radiogroup', { name: /issue status/i })).toBeInTheDocument();
});

View File

@@ -16,10 +16,9 @@ describe('useDebounce', () => {
});
it('updates the debounced value after the delay', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'initial', delay: 500 } }
);
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
initialProps: { value: 'initial', delay: 500 },
});
// Change the value
rerender({ value: 'updated', delay: 500 });
@@ -37,10 +36,9 @@ describe('useDebounce', () => {
});
it('does not update the value before the delay', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'initial', delay: 500 } }
);
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
initialProps: { value: 'initial', delay: 500 },
});
rerender({ value: 'updated', delay: 500 });
@@ -53,10 +51,9 @@ describe('useDebounce', () => {
});
it('resets the timer when value changes rapidly', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'initial', delay: 500 } }
);
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
initialProps: { value: 'initial', delay: 500 },
});
// First change
rerender({ value: 'first', delay: 500 });
@@ -89,10 +86,9 @@ describe('useDebounce', () => {
it('cleans up timeout on unmount', () => {
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
const { unmount, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'initial', delay: 500 } }
);
const { unmount, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
initialProps: { value: 'initial', delay: 500 },
});
rerender({ value: 'updated', delay: 500 });
unmount();
@@ -102,10 +98,9 @@ describe('useDebounce', () => {
});
it('works with different delay values', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'initial', delay: 1000 } }
);
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
initialProps: { value: 'initial', delay: 1000 },
});
rerender({ value: 'updated', delay: 1000 });
@@ -138,10 +133,9 @@ describe('useDebounce', () => {
});
it('handles zero delay', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'initial', delay: 0 } }
);
const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
initialProps: { value: 'initial', delay: 0 },
});
rerender({ value: 'updated', delay: 0 });

View File

@@ -119,9 +119,7 @@ describe('useProjectEvents', () => {
describe('initialization', () => {
it('should start disconnected', () => {
const { result } = renderHook(() =>
useProjectEvents('project-123', { autoConnect: false })
);
const { result } = renderHook(() => useProjectEvents('project-123', { autoConnect: false }));
expect(result.current.connectionState).toBe('disconnected');
expect(result.current.isConnected).toBe(false);

View File

@@ -189,10 +189,9 @@ describe('Event Store', () => {
useEventStore.getState().addEvents([agentEvent, issueEvent, sprintEvent]);
const filtered = useEventStore.getState().getFilteredEvents('project-123', [
EventType.AGENT_MESSAGE,
EventType.ISSUE_CREATED,
]);
const filtered = useEventStore
.getState()
.getFilteredEvents('project-123', [EventType.AGENT_MESSAGE, EventType.ISSUE_CREATED]);
expect(filtered).toHaveLength(2);
expect(filtered.map((e) => e.type)).toContain(EventType.AGENT_MESSAGE);
@@ -210,9 +209,9 @@ describe('Event Store', () => {
});
it('should return empty array for non-existent project', () => {
const filtered = useEventStore.getState().getFilteredEvents('non-existent', [
EventType.AGENT_MESSAGE,
]);
const filtered = useEventStore
.getState()
.getFilteredEvents('non-existent', [EventType.AGENT_MESSAGE]);
expect(filtered).toEqual([]);
});
});