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 }) => { test('approval actions are visible for pending approvals', async ({ page }) => {
// Find approval event // Find approval event
const approvalEvent = page.locator('[data-testid^="event-item-"]', { const approvalEvent = page
.locator('[data-testid^="event-item-"]', {
has: page.getByText('Action Required'), has: page.getByText('Action Required'),
}).first(); })
.first();
// Approval buttons should be visible // Approval buttons should be visible
await expect(approvalEvent.getByTestId('approve-button')).toBeVisible(); 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 }) => { test('should navigate to GitHub when clicking View on GitHub', async ({ page }) => {
const githubLink = page.getByRole('link', { name: /View on GitHub/i }).first(); const githubLink = page.getByRole('link', { name: /View on GitHub/i }).first();
await expect(githubLink).toBeVisible(); 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 }) => { 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(); await expect(page.getByTestId('project-header')).toBeVisible();
// Check project name // 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 // Check status badges
await expect(page.getByText('In Progress')).toBeVisible(); await expect(page.getByText('In Progress')).toBeVisible();
@@ -288,7 +290,9 @@ test.describe('Project Dashboard Activity Feed', () => {
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// Look for action buttons in activity feed (if any require action) // 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(); const count = await reviewButton.count();
// Either there are action items or not - both are valid // Either there are action items or not - both are valid

View File

@@ -7,42 +7,42 @@
* - Please do NOT modify this file. * - Please do NOT modify this file.
*/ */
const PACKAGE_VERSION = '2.12.3' const PACKAGE_VERSION = '2.12.3';
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82';
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const IS_MOCKED_RESPONSE = Symbol('isMockedResponse');
const activeClientIds = new Set() const activeClientIds = new Set();
addEventListener('install', function () { addEventListener('install', function () {
self.skipWaiting() self.skipWaiting();
}) });
addEventListener('activate', function (event) { addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim()) event.waitUntil(self.clients.claim());
}) });
addEventListener('message', async function (event) { addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id') const clientId = Reflect.get(event.source || {}, 'id');
if (!clientId || !self.clients) { if (!clientId || !self.clients) {
return return;
} }
const client = await self.clients.get(clientId) const client = await self.clients.get(clientId);
if (!client) { if (!client) {
return return;
} }
const allClients = await self.clients.matchAll({ const allClients = await self.clients.matchAll({
type: 'window', type: 'window',
}) });
switch (event.data) { switch (event.data) {
case 'KEEPALIVE_REQUEST': { case 'KEEPALIVE_REQUEST': {
sendToClient(client, { sendToClient(client, {
type: 'KEEPALIVE_RESPONSE', type: 'KEEPALIVE_RESPONSE',
}) });
break break;
} }
case 'INTEGRITY_CHECK_REQUEST': { case 'INTEGRITY_CHECK_REQUEST': {
@@ -52,12 +52,12 @@ addEventListener('message', async function (event) {
packageVersion: PACKAGE_VERSION, packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM, checksum: INTEGRITY_CHECKSUM,
}, },
}) });
break break;
} }
case 'MOCK_ACTIVATE': { case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId) activeClientIds.add(clientId);
sendToClient(client, { sendToClient(client, {
type: 'MOCKING_ENABLED', type: 'MOCKING_ENABLED',
@@ -67,54 +67,51 @@ addEventListener('message', async function (event) {
frameType: client.frameType, frameType: client.frameType,
}, },
}, },
}) });
break break;
} }
case 'CLIENT_CLOSED': { case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId) activeClientIds.delete(clientId);
const remainingClients = allClients.filter((client) => { const remainingClients = allClients.filter((client) => {
return client.id !== clientId return client.id !== clientId;
}) });
// Unregister itself when there are no more clients // Unregister itself when there are no more clients
if (remainingClients.length === 0) { if (remainingClients.length === 0) {
self.registration.unregister() self.registration.unregister();
} }
break break;
} }
} }
}) });
addEventListener('fetch', function (event) { addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now() const requestInterceptedAt = Date.now();
// Bypass navigation requests. // Bypass navigation requests.
if (event.request.mode === 'navigate') { if (event.request.mode === 'navigate') {
return return;
} }
// Opening the DevTools triggers the "only-if-cached" request // Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests. // that cannot be handled by the worker. Bypass such requests.
if ( if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') {
event.request.cache === 'only-if-cached' && return;
event.request.mode !== 'same-origin'
) {
return
} }
// Bypass all requests when there are no active clients. // Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests // Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload). // after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) { if (activeClientIds.size === 0) {
return return;
} }
const requestId = crypto.randomUUID() const requestId = crypto.randomUUID();
event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) event.respondWith(handleRequest(event, requestId, requestInterceptedAt));
}) });
/** /**
* @param {FetchEvent} event * @param {FetchEvent} event
@@ -122,23 +119,18 @@ addEventListener('fetch', function (event) {
* @param {number} requestInterceptedAt * @param {number} requestInterceptedAt
*/ */
async function handleRequest(event, requestId, requestInterceptedAt) { async function handleRequest(event, requestId, requestInterceptedAt) {
const client = await resolveMainClient(event) const client = await resolveMainClient(event);
const requestCloneForEvents = event.request.clone() const requestCloneForEvents = event.request.clone();
const response = await getResponse( const response = await getResponse(event, client, requestId, requestInterceptedAt);
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events. // Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise // Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely. // this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) { 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. // Clone the response so both the client and the library could consume it.
const responseClone = response.clone() const responseClone = response.clone();
sendToClient( sendToClient(
client, 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>} * @returns {Promise<Client | undefined>}
*/ */
async function resolveMainClient(event) { 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)) { if (activeClientIds.has(event.clientId)) {
return client return client;
} }
if (client?.frameType === 'top-level') { if (client?.frameType === 'top-level') {
return client return client;
} }
const allClients = await self.clients.matchAll({ const allClients = await self.clients.matchAll({
type: 'window', type: 'window',
}) });
return allClients return allClients
.filter((client) => { .filter((client) => {
// Get only those clients that are currently visible. // Get only those clients that are currently visible.
return client.visibilityState === 'visible' return client.visibilityState === 'visible';
}) })
.find((client) => { .find((client) => {
// Find the client ID that's recorded in the // Find the client ID that's recorded in the
// set of clients that have registered the worker. // 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) { async function getResponse(event, client, requestId, requestInterceptedAt) {
// Clone the request because it might've been already used // Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client). // (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone() const requestClone = event.request.clone();
function passthrough() { function passthrough() {
// Cast the request headers to a new Headers instance // Cast the request headers to a new Headers instance
// so the headers can be manipulated with. // 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. // Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the // This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies. // user-defined CORS policies.
const acceptHeader = headers.get('accept') const acceptHeader = headers.get('accept');
if (acceptHeader) { if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim()) const values = acceptHeader.split(',').map((value) => value.trim());
const filteredValues = values.filter( const filteredValues = values.filter((value) => value !== 'msw/passthrough');
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) { if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', ')) headers.set('accept', filteredValues.join(', '));
} else { } else {
headers.delete('accept') headers.delete('accept');
} }
} }
return fetch(requestClone, { headers }) return fetch(requestClone, { headers });
} }
// Bypass mocking when the client is not active. // Bypass mocking when the client is not active.
if (!client) { if (!client) {
return passthrough() return passthrough();
} }
// Bypass initial page load requests (i.e. static assets). // 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 // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests. // and is not ready to handle requests.
if (!activeClientIds.has(client.id)) { if (!activeClientIds.has(client.id)) {
return passthrough() return passthrough();
} }
// Notify the client that a request has been intercepted. // 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( const clientMessage = await sendToClient(
client, client,
{ {
@@ -263,20 +253,20 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
...serializedRequest, ...serializedRequest,
}, },
}, },
[serializedRequest.body], [serializedRequest.body]
) );
switch (clientMessage.type) { switch (clientMessage.type) {
case 'MOCK_RESPONSE': { case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data) return respondWithMock(clientMessage.data);
} }
case 'PASSTHROUGH': { 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 = []) { function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const channel = new MessageChannel() const channel = new MessageChannel();
channel.port1.onmessage = (event) => { channel.port1.onmessage = (event) => {
if (event.data && event.data.error) { 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, [ client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]);
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 // 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. // a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) { 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, { Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true, value: true,
enumerable: true, enumerable: true,
}) });
return mockedResponse return mockedResponse;
} }
/** /**
@@ -345,5 +332,5 @@ async function serializeRequest(request) {
referrerPolicy: request.referrerPolicy, referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(), body: await request.arrayBuffer(),
keepalive: request.keepalive, keepalive: request.keepalive,
} };
} }

View File

@@ -199,13 +199,10 @@ export default function ActivityFeedPage() {
<BellOff className="h-4 w-4" /> <BellOff className="h-4 w-4" />
)} )}
</Button> </Button>
<Button <Button variant="ghost" size="icon" onClick={reconnect} aria-label="Refresh connection">
variant="ghost" <RefreshCw
size="icon" className={connectionState === 'connecting' ? 'h-4 w-4 animate-spin' : 'h-4 w-4'}
onClick={reconnect} />
aria-label="Refresh connection"
>
<RefreshCw className={connectionState === 'connecting' ? 'h-4 w-4 animate-spin' : 'h-4 w-4'} />
</Button> </Button>
</div> </div>
</div> </div>
@@ -214,7 +211,8 @@ export default function ActivityFeedPage() {
{(!isConnected || sseEvents.length === 0) && ( {(!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"> <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"> <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> </p>
</div> </div>
)} )}

View File

@@ -32,11 +32,7 @@ export default function AgentTypeDetailPage() {
const [viewMode, setViewMode] = useState<ViewMode>(isNew ? 'create' : 'detail'); const [viewMode, setViewMode] = useState<ViewMode>(isNew ? 'create' : 'detail');
// Fetch agent type data (skip if creating new) // Fetch agent type data (skip if creating new)
const { const { data: agentType, isLoading, error } = useAgentType(isNew ? null : id);
data: agentType,
isLoading,
error,
} = useAgentType(isNew ? null : id);
// Mutations // Mutations
const createMutation = useCreateAgentType(); const createMutation = useCreateAgentType();
@@ -171,7 +167,7 @@ export default function AgentTypeDetailPage() {
<div className="container mx-auto px-4 py-6"> <div className="container mx-auto px-4 py-6">
{(viewMode === 'create' || viewMode === 'edit') && ( {(viewMode === 'create' || viewMode === 'edit') && (
<AgentTypeForm <AgentTypeForm
agentType={viewMode === 'edit' ? agentType ?? undefined : undefined} agentType={viewMode === 'edit' ? (agentType ?? undefined) : undefined}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onCancel={handleCancel} onCancel={handleCancel}
isSubmitting={createMutation.isPending || updateMutation.isPending} isSubmitting={createMutation.isPending || updateMutation.isPending}

View File

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

View File

@@ -14,12 +14,7 @@ import { useRouter } from 'next/navigation';
import { Plus, Upload } from 'lucide-react'; import { Plus, Upload } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { import { IssueFilters, IssueTable, BulkActions, useIssues } from '@/features/issues';
IssueFilters,
IssueTable,
BulkActions,
useIssues,
} from '@/features/issues';
import type { IssueFiltersType, IssueSort } from '@/features/issues'; import type { IssueFiltersType, IssueSort } from '@/features/issues';
interface ProjectIssuesPageProps { interface ProjectIssuesPageProps {
@@ -95,11 +90,7 @@ export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {
<p className="mt-2 text-sm text-muted-foreground"> <p className="mt-2 text-sm text-muted-foreground">
Failed to load issues. Please try again later. Failed to load issues. Please try again later.
</p> </p>
<Button <Button variant="outline" className="mt-4" onClick={() => window.location.reload()}>
variant="outline"
className="mt-4"
onClick={() => window.location.reload()}
>
Retry Retry
</Button> </Button>
</div> </div>
@@ -169,26 +160,15 @@ export default function ProjectIssuesPage({ params }: ProjectIssuesPageProps) {
<div className="flex items-center justify-between text-sm text-muted-foreground"> <div className="flex items-center justify-between text-sm text-muted-foreground">
<span> <span>
Showing {(data.pagination.page - 1) * data.pagination.page_size + 1} to{' '} Showing {(data.pagination.page - 1) * data.pagination.page_size + 1} to{' '}
{Math.min( {Math.min(data.pagination.page * data.pagination.page_size, data.pagination.total)} of{' '}
data.pagination.page * data.pagination.page_size, {data.pagination.total} issues
data.pagination.total
)}{' '}
of {data.pagination.total} issues
</span> </span>
{data.pagination.total_pages > 1 && ( {data.pagination.total_pages > 1 && (
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button variant="outline" size="sm" disabled={!data.pagination.has_prev}>
variant="outline"
size="sm"
disabled={!data.pagination.has_prev}
>
Previous Previous
</Button> </Button>
<Button <Button variant="outline" size="sm" disabled={!data.pagination.has_next}>
variant="outline"
size="sm"
disabled={!data.pagination.has_next}
>
Next Next
</Button> </Button>
</div> </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. The Project Dashboard prototype has been created and is ready for review.
### How to View ### How to View
1. Start the frontend dev server: `cd frontend && npm run dev` 1. Start the frontend dev server: `cd frontend && npm run dev`
2. Navigate to: `http://localhost:3000/en/prototypes/project-dashboard` 2. Navigate to: `http://localhost:3000/en/prototypes/project-dashboard`
### What's Included ### What's Included
**Header Section** **Header Section**
- Project name with status badge (In Progress, Completed, Paused, Blocked) - Project name with status badge (In Progress, Completed, Paused, Blocked)
- Autonomy level indicator (Full Control, Milestone, Autonomous) - Autonomy level indicator (Full Control, Milestone, Autonomous)
- Quick action buttons (Pause Project, Run Sprint) - Quick action buttons (Pause Project, Run Sprint)
**Agent Panel** **Agent Panel**
- List of all project agents with avatars - List of all project agents with avatars
- Real-time status indicators (active = green, idle = yellow, pending = gray) - Real-time status indicators (active = green, idle = yellow, pending = gray)
- Current task description for each agent - Current task description for each agent
- Last activity timestamp - Last activity timestamp
**Sprint Overview** **Sprint Overview**
- Current sprint progress bar - Current sprint progress bar
- Issue statistics grid (Completed, In Progress, Blocked, To Do) - Issue statistics grid (Completed, In Progress, Blocked, To Do)
- Visual burndown chart with ideal vs actual lines - Visual burndown chart with ideal vs actual lines
- Sprint selector dropdown - Sprint selector dropdown
**Issue Summary Sidebar** **Issue Summary Sidebar**
- Count of issues by status with color-coded icons - Count of issues by status with color-coded icons
- Quick links to view all issues - Quick links to view all issues
**Recent Activity Feed** **Recent Activity Feed**
- Chronological event list with type icons - Chronological event list with type icons
- Agent attribution - Agent attribution
- Highlighted approval requests with action buttons - Highlighted approval requests with action buttons
### Key Design Decisions ### Key Design Decisions
- Three-column layout on desktop (2/3 main, 1/3 sidebar) - Three-column layout on desktop (2/3 main, 1/3 sidebar)
- Agent status uses traffic light colors for intuitive understanding - Agent status uses traffic light colors for intuitive understanding
- Burndown chart is simplified for quick scanning - Burndown chart is simplified for quick scanning
- Activity feed limited to 5 items with "View All" link - Activity feed limited to 5 items with "View All" link
### Questions for Review ### Questions for Review
1. Is the burndown chart detailed enough? 1. Is the burndown chart detailed enough?
2. Should agent cards be expandable for more details? 2. Should agent cards be expandable for more details?
3. Is the 5-item activity feed sufficient? 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.** **Please review and approve or provide feedback.**
Files: Files:
- `/frontend/src/app/[locale]/prototypes/project-dashboard/page.tsx` - `/frontend/src/app/[locale]/prototypes/project-dashboard/page.tsx`
- `/frontend/src/app/[locale]/prototypes/project-dashboard/README.md` - `/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. The Agent Configuration UI prototype has been created and is ready for review.
### How to View ### How to View
1. Start the frontend dev server: `cd frontend && npm run dev` 1. Start the frontend dev server: `cd frontend && npm run dev`
2. Navigate to: `http://localhost:3000/en/prototypes/agent-configuration` 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 - **Personality Tab**: Large textarea for personality prompt
### Key Design Decisions ### Key Design Decisions
- Separate views for browsing, viewing, and editing - Separate views for browsing, viewing, and editing
- Tabbed editor reduces cognitive load - Tabbed editor reduces cognitive load
- MCP permissions show nested scopes when enabled - MCP permissions show nested scopes when enabled
- Model parameters have helpful descriptions - Model parameters have helpful descriptions
### User Flows to Test ### User Flows to Test
1. Click any card to see detail view 1. Click any card to see detail view
2. Click "Edit" to see editor view 2. Click "Edit" to see editor view
3. Click "Create Agent Type" for blank editor 3. Click "Create Agent Type" for blank editor
4. Navigate tabs in editor 4. Navigate tabs in editor
### Questions for Review ### Questions for Review
1. Is the tabbed editor the right approach? 1. Is the tabbed editor the right approach?
2. Should expertise be free-form tags or predefined list? 2. Should expertise be free-form tags or predefined list?
3. Should model parameters have "presets"? 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.** **Please review and approve or provide feedback.**
Files: Files:
- `/frontend/src/app/[locale]/prototypes/agent-configuration/page.tsx` - `/frontend/src/app/[locale]/prototypes/agent-configuration/page.tsx`
- `/frontend/src/app/[locale]/prototypes/agent-configuration/README.md` - `/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. The Issue List and Detail Views prototype has been created and is ready for review.
### How to View ### How to View
1. Start the frontend dev server: `cd frontend && npm run dev` 1. Start the frontend dev server: `cd frontend && npm run dev`
2. Navigate to: `http://localhost:3000/en/prototypes/issue-management` 2. Navigate to: `http://localhost:3000/en/prototypes/issue-management`
### What's Included ### What's Included
**List View** **List View**
- Filterable table with sortable columns - Filterable table with sortable columns
- Quick status filter + expandable advanced filters - Quick status filter + expandable advanced filters
- Bulk action bar (appears when selecting issues) - 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 - Labels displayed as badges
**Filter Options** **Filter Options**
- Status: Open, In Progress, In Review, Blocked, Done - Status: Open, In Progress, In Review, Blocked, Done
- Priority: High, Medium, Low - Priority: High, Medium, Low
- Sprint: Current sprints, Backlog - 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. - Labels: Feature, Bug, Backend, Frontend, etc.
**Detail View** **Detail View**
- Full issue content (markdown-like display) - Full issue content (markdown-like display)
- Status workflow panel (click to change status) - Status workflow panel (click to change status)
- Assignment panel with agent avatar - 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) - Development section (branch, PR link)
### Key Design Decisions ### Key Design Decisions
- Table layout for density and scannability - Table layout for density and scannability
- Status workflow matches common issue tracker patterns - Status workflow matches common issue tracker patterns
- Sync status indicator shows data freshness - Sync status indicator shows data freshness
- Activity timeline shows issue history - Activity timeline shows issue history
### User Flows to Test ### User Flows to Test
1. Use search and filters 1. Use search and filters
2. Click checkboxes to see bulk actions 2. Click checkboxes to see bulk actions
3. Sort by clicking column headers 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 5. Click status buttons in detail view
### Questions for Review ### Questions for Review
1. Should we add Kanban view as alternative? 1. Should we add Kanban view as alternative?
2. Is the sync indicator clear enough? 2. Is the sync indicator clear enough?
3. Should there be inline editing? 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.** **Please review and approve or provide feedback.**
Files: Files:
- `/frontend/src/app/[locale]/prototypes/issue-management/page.tsx` - `/frontend/src/app/[locale]/prototypes/issue-management/page.tsx`
- `/frontend/src/app/[locale]/prototypes/issue-management/README.md` - `/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. The Real-time Activity Feed prototype has been created and is ready for review.
### How to View ### How to View
1. Start the frontend dev server: `cd frontend && npm run dev` 1. Start the frontend dev server: `cd frontend && npm run dev`
2. Navigate to: `http://localhost:3000/en/prototypes/activity-feed` 2. Navigate to: `http://localhost:3000/en/prototypes/activity-feed`
### What's Included ### What's Included
**Event Types Displayed** **Event Types Displayed**
- Agent Status: Started, paused, resumed, stopped - Agent Status: Started, paused, resumed, stopped
- Agent Message: Updates, questions, progress reports - Agent Message: Updates, questions, progress reports
- Issue Update: Status changes, assignments, creation - 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 - Milestone: Goals achieved, completions
**Features** **Features**
- Real-time connection indicator (pulsing green when connected) - Real-time connection indicator (pulsing green when connected)
- Time-based event grouping (New, Earlier Today, Yesterday, etc.) - Time-based event grouping (New, Earlier Today, Yesterday, etc.)
- Search functionality - Search functionality
@@ -231,6 +256,7 @@ The Real-time Activity Feed prototype has been created and is ready for review.
- Mark all read functionality - Mark all read functionality
### Key Design Decisions ### Key Design Decisions
- Card-based layout for clear event separation - Card-based layout for clear event separation
- Orange left border highlights action-required items - Orange left border highlights action-required items
- Time grouping helps users orient in timeline - 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 - Real-time indicator builds trust in data freshness
### User Flows to Test ### User Flows to Test
1. Scroll through the event feed 1. Scroll through the event feed
2. Click events to expand details 2. Click events to expand details
3. Open filter panel and select filters 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" 5. Click "Mark all read"
### Questions for Review ### Questions for Review
1. Should events be grouped by time or show flat? 1. Should events be grouped by time or show flat?
2. Should there be sound notifications for urgent items? 2. Should there be sound notifications for urgent items?
3. Should users be able to "star" events? 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.** **Please review and approve or provide feedback.**
Files: Files:
- `/frontend/src/app/[locale]/prototypes/activity-feed/page.tsx` - `/frontend/src/app/[locale]/prototypes/activity-feed/page.tsx`
- `/frontend/src/app/[locale]/prototypes/activity-feed/README.md` - `/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 - Issue #39: Real-time Activity Feed
After the user reviews each prototype and provides feedback: After the user reviews each prototype and provides feedback:
1. Iterate on the design based on feedback 1. Iterate on the design based on feedback
2. Get explicit approval 2. Get explicit approval
3. Begin implementation 3. Begin implementation

View File

@@ -1,13 +1,7 @@
'use client'; 'use client';
import Link from 'next/link'; import Link from 'next/link';
import { import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {

View File

@@ -248,12 +248,47 @@ const EVENT_TYPE_CONFIG: Record<
}; };
const FILTER_CATEGORIES = [ 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: '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: 'approval',
{ id: 'project', label: 'Projects', types: [EventType.PROJECT_CREATED, EventType.PROJECT_UPDATED, EventType.PROJECT_ARCHIVED] }, 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 // Fallback based on event category
if (isAgentEvent(event)) { 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)) { 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)) { 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)) { 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)) { 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)) { 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 { function getEventSummary(event: ProjectEvent): string {
@@ -304,7 +374,9 @@ function getEventSummary(event: ProjectEvent): string {
case EventType.ISSUE_UPDATED: case EventType.ISSUE_UPDATED:
return `Issue ${payload.issue_id || ''} updated`; return `Issue ${payload.issue_id || ''} updated`;
case EventType.ISSUE_ASSIGNED: 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: case EventType.ISSUE_CLOSED:
return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed'; return payload.resolution ? `Closed: ${payload.resolution}` : 'Issue closed';
case EventType.SPRINT_STARTED: case EventType.SPRINT_STARTED:
@@ -318,11 +390,15 @@ function getEventSummary(event: ProjectEvent): string {
case EventType.APPROVAL_DENIED: case EventType.APPROVAL_DENIED:
return payload.reason ? `Denied: ${payload.reason}` : 'Approval denied'; return payload.reason ? `Denied: ${payload.reason}` : 'Approval denied';
case EventType.WORKFLOW_STARTED: 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: case EventType.WORKFLOW_STEP_COMPLETED:
return `Step ${payload.step_number}/${payload.total_steps}: ${payload.step_name || 'completed'}`; return `Step ${payload.step_number}/${payload.total_steps}: ${payload.step_name || 'completed'}`;
case EventType.WORKFLOW_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: 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: default:
@@ -391,11 +467,7 @@ function ConnectionIndicator({ state, onReconnect, className }: ConnectionIndica
return ( return (
<div className={cn('flex items-center gap-2', className)} data-testid="connection-indicator"> <div className={cn('flex items-center gap-2', className)} data-testid="connection-indicator">
<span <span
className={cn( className={cn('h-2 w-2 rounded-full', config.color, config.pulse && 'animate-pulse')}
'h-2 w-2 rounded-full',
config.color,
config.pulse && 'animate-pulse'
)}
aria-hidden="true" aria-hidden="true"
/> />
<span className="text-sm text-muted-foreground">{config.label}</span> <span className="text-sm text-muted-foreground">{config.label}</span>
@@ -475,7 +547,10 @@ function FilterPanel({
checked={showPendingOnly} checked={showPendingOnly}
onCheckedChange={(checked) => onShowPendingOnlyChange(checked as boolean)} 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 Show only pending approvals
{pendingCount > 0 && ( {pendingCount > 0 && (
<Badge variant="destructive" className="text-xs"> <Badge variant="destructive" className="text-xs">
@@ -598,20 +673,28 @@ function EventItem({
}} }}
aria-label={expanded ? 'Collapse details' : 'Expand details'} 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> </Button>
</div> </div>
</div> </div>
{/* Expanded Details */} {/* Expanded Details */}
{expanded && (() => { {expanded &&
(() => {
const issueId = payload.issue_id as string | undefined; const issueId = payload.issue_id as string | undefined;
const pullRequest = payload.pullRequest as string | number | undefined; const pullRequest = payload.pullRequest as string | number | undefined;
const documentUrl = payload.documentUrl as string | undefined; const documentUrl = payload.documentUrl as string | undefined;
const progress = payload.progress as number | undefined; const progress = payload.progress as number | undefined;
return ( return (
<div className="mt-3 rounded-md bg-muted/50 p-3 space-y-3" data-testid="event-details"> <div
className="mt-3 rounded-md bg-muted/50 p-3 space-y-3"
data-testid="event-details"
>
{/* Issue/PR Links */} {/* Issue/PR Links */}
{issueId && ( {issueId && (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
@@ -680,7 +763,12 @@ function EventItem({
</Button> </Button>
)} )}
{onReject && ( {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" /> <XCircle className="mr-2 h-4 w-4" />
Reject Reject
</Button> </Button>
@@ -712,7 +800,10 @@ function LoadingSkeleton() {
function EmptyState({ hasFilters }: { hasFilters: boolean }) { function EmptyState({ hasFilters }: { hasFilters: boolean }) {
return ( 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" /> <Activity className="h-12 w-12 mb-4" aria-hidden="true" />
<h3 className="font-semibold">No activity found</h3> <h3 className="font-semibold">No activity found</h3>
<p className="text-sm"> <p className="text-sm">
@@ -894,7 +985,10 @@ export function ActivityFeed({
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
{groupedEvents.map((group) => ( {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"> <div className="mb-3 flex items-center gap-2">
<h3 className="text-sm font-medium text-muted-foreground">{group.label}</h3> <h3 className="text-sm font-medium text-muted-foreground">{group.label}</h3>
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">

View File

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

View File

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

View File

@@ -41,13 +41,19 @@ interface AgentTypeListProps {
function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) { function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
if (isActive) { if (isActive) {
return ( 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 Active
</Badge> </Badge>
); );
} }
return ( 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 Inactive
</Badge> </Badge>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -123,7 +123,13 @@ export interface IssueCountSummary {
export interface ActivityItem { export interface ActivityItem {
id: string; 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; agent?: string;
message: string; message: string;
timestamp: string; timestamp: string;

View File

@@ -73,16 +73,13 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) {
mutationFn: async (projectData: ProjectCreateData): Promise<ProjectResponse> => { mutationFn: async (projectData: ProjectCreateData): Promise<ProjectResponse> => {
// Call the projects API endpoint // Call the projects API endpoint
// Note: The API client already handles authentication via interceptors // Note: The API client already handles authentication via interceptors
const response = await apiClient.instance.post<ProjectResponse>( const response = await apiClient.instance.post<ProjectResponse>('/api/v1/projects', {
'/api/v1/projects',
{
name: projectData.name, name: projectData.name,
slug: projectData.slug, slug: projectData.slug,
description: projectData.description, description: projectData.description,
autonomy_level: projectData.autonomy_level, autonomy_level: projectData.autonomy_level,
settings: projectData.settings, settings: projectData.settings,
} });
);
return response.data; return response.data;
}, },
@@ -123,7 +120,10 @@ export function ProjectWizard({ locale, className }: ProjectWizardProps) {
<Card className="text-center"> <Card className="text-center">
<CardContent className="space-y-6 p-8"> <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"> <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>
<div> <div>
<h2 className="text-2xl font-bold">Project Created Successfully!</h2> <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" /> <ArrowRight className="ml-2 h-4 w-4" aria-hidden="true" />
</Button> </Button>
) : ( ) : (
<Button <Button onClick={handleCreate} disabled={createProjectMutation.isPending}>
onClick={handleCreate}
disabled={createProjectMutation.isPending}
>
{createProjectMutation.isPending ? ( {createProjectMutation.isPending ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" /> <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>
<span>{steps[displayStep - 1]}</span> <span>{steps[displayStep - 1]}</span>
</div> </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) => ( {Array.from({ length: totalSteps }, (_, i) => (
<div <div
key={i} key={i}

View File

@@ -18,9 +18,4 @@ export type {
} from './types'; } from './types';
// Re-export constants // Re-export constants
export { export { complexityOptions, clientModeOptions, autonomyOptions, WIZARD_STEPS } from './constants';
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 { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';

View File

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

View File

@@ -34,7 +34,11 @@ export function ClientModeStep({ state, updateState }: ClientModeStepProps) {
</p> </p>
</div> </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) => { {clientModeOptions.map((option) => {
const Icon = option.icon; const Icon = option.icon;
const isSelected = state.clientMode === option.id; const isSelected = state.clientMode === option.id;

View File

@@ -39,7 +39,11 @@ export function ComplexityStep({ state, updateState }: ComplexityStepProps) {
)} )}
</div> </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) => { {complexityOptions.map((option) => {
const Icon = option.icon; const Icon = option.icon;
const isSelected = state.complexity === option.id; const isSelected = state.complexity === option.id;

View File

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

View File

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

View File

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

View File

@@ -44,9 +44,7 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
</div> </div>
<div> <div>
<p className="font-medium">{issue.assignee.name}</p> <p className="font-medium">{issue.assignee.name}</p>
<p className="text-xs text-muted-foreground capitalize"> <p className="text-xs text-muted-foreground capitalize">{issue.assignee.type}</p>
{issue.assignee.type}
</p>
</div> </div>
</div> </div>
) : ( ) : (
@@ -92,9 +90,7 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
{issue.due_date && ( {issue.due_date && (
<div> <div>
<p className="text-sm text-muted-foreground">Due Date</p> <p className="text-sm text-muted-foreground">Due Date</p>
<p className="font-medium"> <p className="font-medium">{new Date(issue.due_date).toLocaleDateString()}</p>
{new Date(issue.due_date).toLocaleDateString()}
</p>
</div> </div>
)} )}
@@ -136,19 +132,13 @@ export function IssueDetailPanel({ issue, className }: IssueDetailPanelProps) {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{issue.branch && ( {issue.branch && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<GitBranch <GitBranch className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
className="h-4 w-4 text-muted-foreground"
aria-hidden="true"
/>
<span className="font-mono text-sm">{issue.branch}</span> <span className="font-mono text-sm">{issue.branch}</span>
</div> </div>
)} )}
{issue.pull_request && ( {issue.pull_request && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<GitPullRequest <GitPullRequest className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
className="h-4 w-4 text-muted-foreground"
aria-hidden="true"
/>
<span className="text-sm">{issue.pull_request}</span> <span className="text-sm">{issue.pull_request}</span>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
Open 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="grid gap-4 sm:grid-cols-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="priority-filter">Priority</Label> <Label htmlFor="priority-filter">Priority</Label>
<Select <Select value={filters.priority || 'all'} onValueChange={handlePriorityChange}>
value={filters.priority || 'all'}
onValueChange={handlePriorityChange}
>
<SelectTrigger id="priority-filter"> <SelectTrigger id="priority-filter">
<SelectValue placeholder="All" /> <SelectValue placeholder="All" />
</SelectTrigger> </SelectTrigger>
@@ -172,10 +169,7 @@ export function IssueFilters({ filters, onFiltersChange, className }: IssueFilte
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="assignee-filter">Assignee</Label> <Label htmlFor="assignee-filter">Assignee</Label>
<Select <Select value={filters.assignee || 'all'} onValueChange={handleAssigneeChange}>
value={filters.assignee || 'all'}
onValueChange={handleAssigneeChange}
>
<SelectTrigger id="assignee-filter"> <SelectTrigger id="assignee-filter">
<SelectValue placeholder="All" /> <SelectValue placeholder="All" />
</SelectTrigger> </SelectTrigger>

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,4 +5,8 @@
*/ */
export { useDebounce } from './useDebounce'; 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; mountedRef.current = false;
cleanup(); cleanup();
}; };
}, [autoConnect, isAuthenticated, accessToken, projectId, connectionState, connect, disconnect, cleanup]); }, [
autoConnect,
isAuthenticated,
accessToken,
projectId,
connectionState,
connect,
disconnect,
cleanup,
]);
return { return {
events, events,

View File

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

View File

@@ -245,7 +245,10 @@ describe('HomePage', () => {
const githubLinks = screen.getAllByRole('link', { name: /GitHub/i }); const githubLinks = screen.getAllByRole('link', { name: /GitHub/i });
expect(githubLinks.length).toBeGreaterThan(0); expect(githubLinks.length).toBeGreaterThan(0);
// Syndarix uses Gitea for version control // 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')); await user.click(within(eventItem).getByTestId('approve-button'));
expect(onApprove).toHaveBeenCalledTimes(1); expect(onApprove).toHaveBeenCalledTimes(1);
expect(onApprove).toHaveBeenCalledWith( expect(onApprove).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
expect.objectContaining({ id: 'event-001' })
);
}); });
it('calls onReject when reject button clicked', async () => { it('calls onReject when reject button clicked', async () => {
@@ -370,9 +368,7 @@ describe('ActivityFeed', () => {
await user.click(within(eventItem).getByTestId('reject-button')); await user.click(within(eventItem).getByTestId('reject-button'));
expect(onReject).toHaveBeenCalledTimes(1); expect(onReject).toHaveBeenCalledTimes(1);
expect(onReject).toHaveBeenCalledWith( expect(onReject).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
expect.objectContaining({ id: 'event-001' })
);
}); });
it('shows pending count badge', () => { it('shows pending count badge', () => {
@@ -440,9 +436,7 @@ describe('ActivityFeed', () => {
await user.click(screen.getByTestId('event-item-event-001')); await user.click(screen.getByTestId('event-item-event-001'));
expect(onEventClick).toHaveBeenCalledTimes(1); expect(onEventClick).toHaveBeenCalledTimes(1);
expect(onEventClick).toHaveBeenCalledWith( expect(onEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: 'event-001' }));
expect.objectContaining({ id: 'event-001' })
);
}); });
}); });
@@ -459,7 +453,9 @@ describe('ActivityFeed', () => {
describe('Accessibility', () => { describe('Accessibility', () => {
it('has proper ARIA labels for interactive elements', () => { 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(); expect(screen.getByLabelText('Reconnect')).toBeInTheDocument();
}); });

View File

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

View File

@@ -36,9 +36,7 @@ describe('AgentTypeForm', () => {
it('renders create form title', () => { it('renders create form title', () => {
render(<AgentTypeForm {...defaultProps} />); render(<AgentTypeForm {...defaultProps} />);
expect(screen.getByText('Create Agent Type')).toBeInTheDocument(); expect(screen.getByText('Create Agent Type')).toBeInTheDocument();
expect( expect(screen.getByText('Define a new agent type template')).toBeInTheDocument();
screen.getByText('Define a new agent type template')
).toBeInTheDocument();
}); });
it('renders all tabs', () => { it('renders all tabs', () => {
@@ -233,9 +231,7 @@ describe('AgentTypeForm', () => {
}); });
it('applies custom className', () => { it('applies custom className', () => {
const { container } = render( const { container } = render(<AgentTypeForm {...defaultProps} className="custom-class" />);
<AgentTypeForm {...defaultProps} className="custom-class" />
);
expect(container.querySelector('form')).toHaveClass('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', () => { it('shows empty state when no agent types', () => {
render(<AgentTypeList {...defaultProps} agentTypes={[]} />); render(<AgentTypeList {...defaultProps} agentTypes={[]} />);
expect(screen.getByText('No agent types found')).toBeInTheDocument(); expect(screen.getByText('No agent types found')).toBeInTheDocument();
expect( expect(screen.getByText('Create your first agent type to get started')).toBeInTheDocument();
screen.getByText('Create your first agent type to get started')
).toBeInTheDocument();
}); });
it('shows filter hint in empty state when filters are applied', () => { it('shows filter hint in empty state when filters are applied', () => {
render( render(<AgentTypeList {...defaultProps} agentTypes={[]} searchQuery="nonexistent" />);
<AgentTypeList {...defaultProps} agentTypes={[]} searchQuery="nonexistent" />
);
expect(screen.getByText('Try adjusting your search or filters')).toBeInTheDocument(); expect(screen.getByText('Try adjusting your search or filters')).toBeInTheDocument();
}); });
@@ -175,9 +171,7 @@ describe('AgentTypeList', () => {
}); });
it('applies custom className', () => { it('applies custom className', () => {
const { container } = render( const { container } = render(<AgentTypeList {...defaultProps} className="custom-class" />);
<AgentTypeList {...defaultProps} className="custom-class" />
);
expect(container.firstChild).toHaveClass('custom-class'); expect(container.firstChild).toHaveClass('custom-class');
}); });
}); });

View File

@@ -182,9 +182,7 @@ describe('ConnectionStatus', () => {
describe('className prop', () => { describe('className prop', () => {
it('applies custom className', () => { it('applies custom className', () => {
const { container } = render( const { container } = render(<ConnectionStatus state="connected" className="custom-class" />);
<ConnectionStatus state="connected" className="custom-class" />
);
expect(container.querySelector('.custom-class')).toBeInTheDocument(); 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 }); 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('target', '_blank');
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer'); expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
}); });

View File

@@ -221,7 +221,10 @@ describe('Header', () => {
const mobileGithubLink = githubLinks[1]; const mobileGithubLink = githubLinks[1];
fireEvent.click(mobileGithubLink); fireEvent.click(mobileGithubLink);
// Syndarix uses Gitea for version control // 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 }); 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('target', '_blank');
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer'); expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
}); });

View File

@@ -123,12 +123,7 @@ describe('AppHeader', () => {
}); });
it('displays current project name', () => { it('displays current project name', () => {
render( render(<AppHeader projects={mockProjects} currentProject={mockProjects[0]} />);
<AppHeader
projects={mockProjects}
currentProject={mockProjects[0]}
/>
);
// Multiple instances may show the project name // Multiple instances may show the project name
expect(screen.getAllByText('Project One').length).toBeGreaterThan(0); expect(screen.getAllByText('Project One').length).toBeGreaterThan(0);
@@ -137,12 +132,7 @@ describe('AppHeader', () => {
it('calls onProjectChange when project is changed', async () => { it('calls onProjectChange when project is changed', async () => {
const mockOnChange = jest.fn(); const mockOnChange = jest.fn();
render( render(<AppHeader projects={mockProjects} onProjectChange={mockOnChange} />);
<AppHeader
projects={mockProjects}
onProjectChange={mockOnChange}
/>
);
// The actual test of project switching is in ProjectSwitcher.test.tsx // The actual test of project switching is in ProjectSwitcher.test.tsx
// Here we just verify the prop is passed by checking switcher exists // 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', () => { it('passes custom breadcrumbs to AppBreadcrumbs', () => {
const customBreadcrumbs = [ const customBreadcrumbs = [{ label: 'Custom', href: '/custom', current: true }];
{ label: 'Custom', href: '/custom', current: true },
];
render( render(
<AppLayout breadcrumbs={customBreadcrumbs}> <AppLayout breadcrumbs={customBreadcrumbs}>
@@ -344,10 +342,7 @@ describe('PageHeader', () => {
it('renders actions when provided', () => { it('renders actions when provided', () => {
render( render(
<PageHeader <PageHeader title="Title" actions={<button data-testid="action-button">Action</button>} />
title="Title"
actions={<button data-testid="action-button">Action</button>}
/>
); );
expect(screen.getByTestId('action-button')).toBeInTheDocument(); expect(screen.getByTestId('action-button')).toBeInTheDocument();

View File

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

View File

@@ -74,7 +74,12 @@ const mockUseProjectEventsDefault = {
events: [] as ProjectEvent[], events: [] as ProjectEvent[],
isConnected: true, isConnected: true,
connectionState: 'connected' as ConnectionState, 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, retryCount: 0,
reconnect: mockReconnect, reconnect: mockReconnect,
disconnect: mockDisconnect, disconnect: mockDisconnect,
@@ -389,11 +394,7 @@ describe('Event to Activity Conversion', () => {
}); });
it('handles system actor type', () => { it('handles system actor type', () => {
const event = createMockEvent( const event = createMockEvent(EventType.SPRINT_STARTED, { sprint_name: 'Sprint 5' }, 'system');
EventType.SPRINT_STARTED,
{ sprint_name: 'Sprint 5' },
'system'
);
mockUseProjectEventsResult.events = [event]; mockUseProjectEventsResult.events = [event];
render(<ProjectDashboard projectId="test" />); render(<ProjectDashboard projectId="test" />);
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument(); 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', () => { it('shows pause button when canPause is true and project is active', () => {
const onPauseProject = jest.fn(); const onPauseProject = jest.fn();
render( render(<ProjectHeader project={mockProject} canPause={true} onPauseProject={onPauseProject} />);
<ProjectHeader
project={mockProject}
canPause={true}
onPauseProject={onPauseProject}
/>
);
expect(screen.getByRole('button', { name: /pause project/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /pause project/i })).toBeInTheDocument();
}); });
@@ -64,13 +58,7 @@ describe('ProjectHeader', () => {
it('shows run sprint button when canStart is true', () => { it('shows run sprint button when canStart is true', () => {
const onStartSprint = jest.fn(); const onStartSprint = jest.fn();
render( render(<ProjectHeader project={mockProject} canStart={true} onStartSprint={onStartSprint} />);
<ProjectHeader
project={mockProject}
canStart={true}
onStartSprint={onStartSprint}
/>
);
expect(screen.getByRole('button', { name: /run sprint/i })).toBeInTheDocument(); 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 () => { it('calls onStartSprint when run sprint button is clicked', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const onStartSprint = jest.fn(); const onStartSprint = jest.fn();
render( render(<ProjectHeader project={mockProject} canStart={true} onStartSprint={onStartSprint} />);
<ProjectHeader
project={mockProject}
canStart={true}
onStartSprint={onStartSprint}
/>
);
await user.click(screen.getByRole('button', { name: /run sprint/i })); await user.click(screen.getByRole('button', { name: /run sprint/i }));
expect(onStartSprint).toHaveBeenCalledTimes(1); expect(onStartSprint).toHaveBeenCalledTimes(1);
@@ -98,13 +80,7 @@ describe('ProjectHeader', () => {
it('calls onPauseProject when pause button is clicked', async () => { it('calls onPauseProject when pause button is clicked', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const onPauseProject = jest.fn(); const onPauseProject = jest.fn();
render( render(<ProjectHeader project={mockProject} canPause={true} onPauseProject={onPauseProject} />);
<ProjectHeader
project={mockProject}
canPause={true}
onPauseProject={onPauseProject}
/>
);
await user.click(screen.getByRole('button', { name: /pause project/i })); await user.click(screen.getByRole('button', { name: /pause project/i }));
expect(onPauseProject).toHaveBeenCalledTimes(1); expect(onPauseProject).toHaveBeenCalledTimes(1);
@@ -113,12 +89,7 @@ describe('ProjectHeader', () => {
it('calls onCreateSprint when new sprint button is clicked', async () => { it('calls onCreateSprint when new sprint button is clicked', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const onCreateSprint = jest.fn(); const onCreateSprint = jest.fn();
render( render(<ProjectHeader project={mockProject} onCreateSprint={onCreateSprint} />);
<ProjectHeader
project={mockProject}
onCreateSprint={onCreateSprint}
/>
);
await user.click(screen.getByRole('button', { name: /new sprint/i })); await user.click(screen.getByRole('button', { name: /new sprint/i }));
expect(onCreateSprint).toHaveBeenCalledTimes(1); expect(onCreateSprint).toHaveBeenCalledTimes(1);
@@ -127,12 +98,7 @@ describe('ProjectHeader', () => {
it('calls onSettings when settings button is clicked', async () => { it('calls onSettings when settings button is clicked', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const onSettings = jest.fn(); const onSettings = jest.fn();
render( render(<ProjectHeader project={mockProject} onSettings={onSettings} />);
<ProjectHeader
project={mockProject}
onSettings={onSettings}
/>
);
await user.click(screen.getByRole('button', { name: /project settings/i })); await user.click(screen.getByRole('button', { name: /project settings/i }));
expect(onSettings).toHaveBeenCalledTimes(1); expect(onSettings).toHaveBeenCalledTimes(1);

View File

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

View File

@@ -23,9 +23,7 @@ describe('ProjectStatusBadge', () => {
}); });
it('applies custom className', () => { it('applies custom className', () => {
const { container } = render( const { container } = render(<ProjectStatusBadge status="active" className="custom-class" />);
<ProjectStatusBadge status="active" className="custom-class" />
);
expect(container.firstChild).toHaveClass('custom-class'); expect(container.firstChild).toHaveClass('custom-class');
}); });
}); });
@@ -54,9 +52,7 @@ describe('AutonomyBadge', () => {
}); });
it('applies custom className', () => { it('applies custom className', () => {
const { container } = render( const { container } = render(<AutonomyBadge level="milestone" className="custom-class" />);
<AutonomyBadge level="milestone" className="custom-class" />
);
expect(container.firstChild).toHaveClass('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 user.click(screen.getByRole('button', { name: /create project/i }));
await waitFor(() => { 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 })); 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', () => { it('has accessible radiogroup role', () => {
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />); 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(); const user = userEvent.setup();
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />); 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); await user.click(fullControlOption);
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'full_control' }); expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'full_control' });
@@ -89,7 +93,9 @@ describe('AutonomyStep', () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<AutonomyStep state={defaultState} updateState={mockUpdateState} />); 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); await user.click(autonomousOption);
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' }); expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });
@@ -180,7 +186,7 @@ describe('AutonomyStep', () => {
autonomyOptions.forEach((option) => { autonomyOptions.forEach((option) => {
const button = screen.getByRole('button', { const button = screen.getByRole('button', {
name: new RegExp(`${option.label}.*${option.description}`, 'i') name: new RegExp(`${option.label}.*${option.description}`, 'i'),
}); });
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
}); });
@@ -207,7 +213,9 @@ describe('AutonomyStep', () => {
}; };
render(<AutonomyStep state={stateWithFullControl} updateState={mockUpdateState} />); 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); await user.click(autonomousOption);
expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' }); expect(mockUpdateState).toHaveBeenCalledWith({ autonomyLevel: 'autonomous' });

View File

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

View File

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

View File

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

View File

@@ -14,9 +14,7 @@ describe('StatusWorkflow', () => {
}); });
it('renders all status options', () => { it('renders all status options', () => {
render( render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
);
expect(screen.getByText('Open')).toBeInTheDocument(); expect(screen.getByText('Open')).toBeInTheDocument();
expect(screen.getByText('In Progress')).toBeInTheDocument(); expect(screen.getByText('In Progress')).toBeInTheDocument();
@@ -26,9 +24,7 @@ describe('StatusWorkflow', () => {
}); });
it('highlights current status', () => { it('highlights current status', () => {
render( render(<StatusWorkflow currentStatus="in_progress" onStatusChange={mockOnStatusChange} />);
<StatusWorkflow currentStatus="in_progress" onStatusChange={mockOnStatusChange} />
);
const inProgressButton = screen.getByRole('radio', { name: /in progress/i }); const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
expect(inProgressButton).toHaveAttribute('aria-checked', 'true'); expect(inProgressButton).toHaveAttribute('aria-checked', 'true');
@@ -36,9 +32,7 @@ describe('StatusWorkflow', () => {
it('calls onStatusChange when status is clicked', async () => { it('calls onStatusChange when status is clicked', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render( render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
);
const inProgressButton = screen.getByRole('radio', { name: /in progress/i }); const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
await user.click(inProgressButton); await user.click(inProgressButton);
@@ -48,9 +42,7 @@ describe('StatusWorkflow', () => {
it('disables status buttons when disabled prop is true', async () => { it('disables status buttons when disabled prop is true', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render( render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} disabled />);
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} disabled />
);
const inProgressButton = screen.getByRole('radio', { name: /in progress/i }); const inProgressButton = screen.getByRole('radio', { name: /in progress/i });
expect(inProgressButton).toBeDisabled(); expect(inProgressButton).toBeDisabled();
@@ -72,9 +64,7 @@ describe('StatusWorkflow', () => {
}); });
it('has proper radiogroup role', () => { it('has proper radiogroup role', () => {
render( render(<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />);
<StatusWorkflow currentStatus="open" onStatusChange={mockOnStatusChange} />
);
expect(screen.getByRole('radiogroup', { name: /issue status/i })).toBeInTheDocument(); 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', () => { it('updates the debounced value after the delay', () => {
const { result, rerender } = renderHook( const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
({ value, delay }) => useDebounce(value, delay), initialProps: { value: 'initial', delay: 500 },
{ initialProps: { value: 'initial', delay: 500 } } });
);
// Change the value // Change the value
rerender({ value: 'updated', delay: 500 }); rerender({ value: 'updated', delay: 500 });
@@ -37,10 +36,9 @@ describe('useDebounce', () => {
}); });
it('does not update the value before the delay', () => { it('does not update the value before the delay', () => {
const { result, rerender } = renderHook( const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
({ value, delay }) => useDebounce(value, delay), initialProps: { value: 'initial', delay: 500 },
{ initialProps: { value: 'initial', delay: 500 } } });
);
rerender({ value: 'updated', delay: 500 }); rerender({ value: 'updated', delay: 500 });
@@ -53,10 +51,9 @@ describe('useDebounce', () => {
}); });
it('resets the timer when value changes rapidly', () => { it('resets the timer when value changes rapidly', () => {
const { result, rerender } = renderHook( const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
({ value, delay }) => useDebounce(value, delay), initialProps: { value: 'initial', delay: 500 },
{ initialProps: { value: 'initial', delay: 500 } } });
);
// First change // First change
rerender({ value: 'first', delay: 500 }); rerender({ value: 'first', delay: 500 });
@@ -89,10 +86,9 @@ describe('useDebounce', () => {
it('cleans up timeout on unmount', () => { it('cleans up timeout on unmount', () => {
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
const { unmount, rerender } = renderHook( const { unmount, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
({ value, delay }) => useDebounce(value, delay), initialProps: { value: 'initial', delay: 500 },
{ initialProps: { value: 'initial', delay: 500 } } });
);
rerender({ value: 'updated', delay: 500 }); rerender({ value: 'updated', delay: 500 });
unmount(); unmount();
@@ -102,10 +98,9 @@ describe('useDebounce', () => {
}); });
it('works with different delay values', () => { it('works with different delay values', () => {
const { result, rerender } = renderHook( const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
({ value, delay }) => useDebounce(value, delay), initialProps: { value: 'initial', delay: 1000 },
{ initialProps: { value: 'initial', delay: 1000 } } });
);
rerender({ value: 'updated', delay: 1000 }); rerender({ value: 'updated', delay: 1000 });
@@ -138,10 +133,9 @@ describe('useDebounce', () => {
}); });
it('handles zero delay', () => { it('handles zero delay', () => {
const { result, rerender } = renderHook( const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), {
({ value, delay }) => useDebounce(value, delay), initialProps: { value: 'initial', delay: 0 },
{ initialProps: { value: 'initial', delay: 0 } } });
);
rerender({ value: 'updated', delay: 0 }); rerender({ value: 'updated', delay: 0 });

View File

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

View File

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