Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions codev/projects/700-dashboard-backlog-show-assigne/status.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
id: '700'
title: dashboard-backlog-show-assigne
protocol: air
phase: pr
plan_phases: []
current_plan_phase: null
gates:
pr:
status: pending
iteration: 1
build_complete: false
history: []
started_at: '2026-04-26T01:12:08.135Z'
updated_at: '2026-04-26T01:17:50.460Z'
4 changes: 2 additions & 2 deletions packages/codev/scripts/forge/github/issue-list.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
# Forge concept: issue-list (GitHub via gh CLI)
# Output: JSON [{number, title, url, labels, createdAt, author}]
exec gh issue list --limit 200 --json number,title,url,labels,createdAt,author
# Output: JSON [{number, title, url, labels, createdAt, author, assignees}]
exec gh issue list --limit 200 --json number,title,url,labels,createdAt,author,assignees
27 changes: 27 additions & 0 deletions packages/codev/src/agent-farm/__tests__/overview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,33 @@ describe('overview', () => {
const backlog = deriveBacklog(issues, tmpDir, new Set(), new Set());
expect(backlog[0].author).toBeUndefined();
});

it('maps a single assignee login', () => {
const issues = [{ ...issueItem(42, 'Test'), assignees: [{ login: 'amr' }] }];

const backlog = deriveBacklog(issues, tmpDir, new Set(), new Set());
expect(backlog[0].assignees).toEqual(['amr']);
});

it('maps multiple assignee logins', () => {
const issues = [
{ ...issueItem(42, 'Test'), assignees: [{ login: 'amr' }, { login: 'bob' }] },
];

const backlog = deriveBacklog(issues, tmpDir, new Set(), new Set());
expect(backlog[0].assignees).toEqual(['amr', 'bob']);
});

it('omits assignees when array is empty or missing', () => {
const empty = [{ ...issueItem(42, 'Test'), assignees: [] }];
const missing = [issueItem(43, 'Test')];

const backlogEmpty = deriveBacklog(empty, tmpDir, new Set(), new Set());
const backlogMissing = deriveBacklog(missing, tmpDir, new Set(), new Set());

expect(backlogEmpty[0].assignees).toBeUndefined();
expect(backlogMissing[0].assignees).toBeUndefined();
});
});

// ==========================================================================
Expand Down
3 changes: 3 additions & 0 deletions packages/codev/src/agent-farm/servers/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface BacklogItem {
hasBuilder: boolean;
createdAt: string;
author?: string;
assignees?: string[];
specPath?: string;
planPath?: string;
reviewPath?: string;
Expand Down Expand Up @@ -659,6 +660,8 @@ export function deriveBacklog(
createdAt: issue.createdAt,
author: issue.author?.login,
};
const assignees = issue.assignees?.map(a => a.login) ?? [];
if (assignees.length > 0) item.assignees = assignees;
if (specFile) item.specPath = `codev/specs/${specFile}`;
if (planFile) item.planPath = `codev/plans/${planFile}`;
if (reviewFile) item.reviewPath = `codev/reviews/${reviewFile}`;
Expand Down
1 change: 1 addition & 0 deletions packages/codev/src/lib/forge-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface IssueListItem {
createdAt: string;
closedAt?: string;
author?: { login: string };
assignees?: Array<{ login: string }>;
}

/** Output of the `issue-list` concept command. */
Expand Down
60 changes: 60 additions & 0 deletions packages/dashboard/__tests__/BacklogList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, it, expect, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import { BacklogList } from '../src/components/BacklogList.js';
import type { OverviewBacklogItem } from '../src/lib/api.js';

afterEach(() => {
cleanup();
});

function makeItem(overrides: Partial<OverviewBacklogItem> = {}): OverviewBacklogItem {
return {
id: '1',
title: 'Test issue',
url: 'https://github.com/org/repo/issues/1',
type: 'project',
priority: 'medium',
hasSpec: false,
hasPlan: false,
hasReview: false,
hasBuilder: false,
createdAt: new Date().toISOString(),
author: 'waleedkadous',
...overrides,
};
}

describe('BacklogList assignee rendering', () => {
it('renders "a: none" when there are no assignees', () => {
const items = [makeItem({ id: '1', title: 'Unassigned' })];
render(<BacklogList items={items} />);

expect(screen.getByText('r: @waleedkadous')).toBeInTheDocument();
expect(screen.getByText('a: none')).toBeInTheDocument();
});

it('renders a single assignee as "a: @login"', () => {
const items = [
makeItem({ id: '2', title: 'One assignee', assignees: ['amr'] }),
];
render(<BacklogList items={items} />);

expect(screen.getByText('a: @amr')).toBeInTheDocument();
});

it('renders multiple assignees comma-separated', () => {
const items = [
makeItem({ id: '3', title: 'Many assignees', assignees: ['amr', 'bob'] }),
];
render(<BacklogList items={items} />);

expect(screen.getByText('a: @amr, @bob')).toBeInTheDocument();
});

it('treats empty assignees array as no assignees', () => {
const items = [makeItem({ id: '4', title: 'Empty list', assignees: [] })];
render(<BacklogList items={items} />);

expect(screen.getByText('a: none')).toBeInTheDocument();
});
});
9 changes: 8 additions & 1 deletion packages/dashboard/src/components/BacklogList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,14 @@ export function BacklogList({ items, onRefresh }: BacklogListProps) {
<span className="backlog-row-number">#{item.id}</span>
<span className={`backlog-type-tag ${TYPE_CLASS[item.type] ?? ''}`}>{item.type}</span>
<span className="backlog-row-title">{item.title}</span>
{item.author && <span className="backlog-row-author">@{item.author}</span>}
{item.author && (
<span className="backlog-row-author">r: @{item.author}</span>
)}
<span className="backlog-row-assignees">
a: {item.assignees && item.assignees.length > 0
? item.assignees.map(a => `@${a}`).join(', ')
: 'none'}
</span>
<span className="backlog-row-age">{timeAgo(item.createdAt)}</span>
</a>
{(item.specPath || item.planPath || item.reviewPath) && (
Expand Down
3 changes: 2 additions & 1 deletion packages/dashboard/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1408,7 +1408,8 @@ a.attention-row {
white-space: nowrap;
}

.backlog-row-author {
.backlog-row-author,
.backlog-row-assignees {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export interface OverviewBacklogItem {
hasBuilder: boolean;
createdAt: string;
author?: string;
assignees?: string[];
specPath?: string;
planPath?: string;
reviewPath?: string;
Expand Down
Loading