From 5d9a2b0a8e15ad8f5d8af2104a254320d810ff83 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Sat, 25 Apr 2026 18:12:08 -0700 Subject: [PATCH 1/3] chore(porch): 700 init air --- .../700-dashboard-backlog-show-assigne/status.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 codev/projects/700-dashboard-backlog-show-assigne/status.yaml diff --git a/codev/projects/700-dashboard-backlog-show-assigne/status.yaml b/codev/projects/700-dashboard-backlog-show-assigne/status.yaml new file mode 100644 index 00000000..8b8f3c13 --- /dev/null +++ b/codev/projects/700-dashboard-backlog-show-assigne/status.yaml @@ -0,0 +1,14 @@ +id: '700' +title: dashboard-backlog-show-assigne +protocol: air +phase: implement +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:12:08.136Z' From 7d94e808a45ed701fb19bedf58e97854f9e0bc14 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Sat, 25 Apr 2026 18:17:22 -0700 Subject: [PATCH 2/3] [#700] Show assignee alongside reporter in dashboard backlog Display each backlog row with explicit r:/a: labels so the assignee is visible at a glance: r: @waleedkadous a: none r: @waleedkadous a: @amr, @bob Plumb assignees through the four affected layers: - forge-contracts: add assignees field to IssueListItem - gh issue-list script: include assignees in --json flag - overview server: map issue.assignees into BacklogItem.assignees - shared API types: add assignees? to OverviewBacklogItem - BacklogList: render r:/a: labels (always show a:, "none" when empty) Tests cover the three required cases (none, one, many) at both the backend mapping layer (deriveBacklog) and the UI layer (BacklogList). --- .../codev/scripts/forge/github/issue-list.sh | 4 +- .../src/agent-farm/__tests__/overview.test.ts | 27 +++++++++ .../codev/src/agent-farm/servers/overview.ts | 3 + packages/codev/src/lib/forge-contracts.ts | 1 + .../dashboard/__tests__/BacklogList.test.tsx | 60 +++++++++++++++++++ .../dashboard/src/components/BacklogList.tsx | 9 ++- packages/dashboard/src/index.css | 3 +- packages/types/src/api.ts | 1 + 8 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 packages/dashboard/__tests__/BacklogList.test.tsx diff --git a/packages/codev/scripts/forge/github/issue-list.sh b/packages/codev/scripts/forge/github/issue-list.sh index 477e3968..3b2a490f 100755 --- a/packages/codev/scripts/forge/github/issue-list.sh +++ b/packages/codev/scripts/forge/github/issue-list.sh @@ -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 diff --git a/packages/codev/src/agent-farm/__tests__/overview.test.ts b/packages/codev/src/agent-farm/__tests__/overview.test.ts index befb9ed3..eadf5417 100644 --- a/packages/codev/src/agent-farm/__tests__/overview.test.ts +++ b/packages/codev/src/agent-farm/__tests__/overview.test.ts @@ -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(); + }); }); // ========================================================================== diff --git a/packages/codev/src/agent-farm/servers/overview.ts b/packages/codev/src/agent-farm/servers/overview.ts index 7c3ebed8..6a6cc3f3 100644 --- a/packages/codev/src/agent-farm/servers/overview.ts +++ b/packages/codev/src/agent-farm/servers/overview.ts @@ -70,6 +70,7 @@ export interface BacklogItem { hasBuilder: boolean; createdAt: string; author?: string; + assignees?: string[]; specPath?: string; planPath?: string; reviewPath?: string; @@ -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}`; diff --git a/packages/codev/src/lib/forge-contracts.ts b/packages/codev/src/lib/forge-contracts.ts index 4603d776..d25f9634 100644 --- a/packages/codev/src/lib/forge-contracts.ts +++ b/packages/codev/src/lib/forge-contracts.ts @@ -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. */ diff --git a/packages/dashboard/__tests__/BacklogList.test.tsx b/packages/dashboard/__tests__/BacklogList.test.tsx new file mode 100644 index 00000000..15219663 --- /dev/null +++ b/packages/dashboard/__tests__/BacklogList.test.tsx @@ -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 { + 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(); + + 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(); + + expect(screen.getByText('a: @amr')).toBeInTheDocument(); + }); + + it('renders multiple assignees comma-separated', () => { + const items = [ + makeItem({ id: '3', title: 'Many assignees', assignees: ['amr', 'bob'] }), + ]; + render(); + + 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(); + + expect(screen.getByText('a: none')).toBeInTheDocument(); + }); +}); diff --git a/packages/dashboard/src/components/BacklogList.tsx b/packages/dashboard/src/components/BacklogList.tsx index 06798d1e..ea0c5261 100644 --- a/packages/dashboard/src/components/BacklogList.tsx +++ b/packages/dashboard/src/components/BacklogList.tsx @@ -60,7 +60,14 @@ export function BacklogList({ items, onRefresh }: BacklogListProps) { #{item.id} {item.type} {item.title} - {item.author && @{item.author}} + {item.author && ( + r: @{item.author} + )} + + a: {item.assignees && item.assignees.length > 0 + ? item.assignees.map(a => `@${a}`).join(', ') + : 'none'} + {timeAgo(item.createdAt)} {(item.specPath || item.planPath || item.reviewPath) && ( diff --git a/packages/dashboard/src/index.css b/packages/dashboard/src/index.css index fd375830..9bba21f7 100644 --- a/packages/dashboard/src/index.css +++ b/packages/dashboard/src/index.css @@ -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; diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index ffb6d970..678d0520 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -110,6 +110,7 @@ export interface OverviewBacklogItem { hasBuilder: boolean; createdAt: string; author?: string; + assignees?: string[]; specPath?: string; planPath?: string; reviewPath?: string; From dde47527795ab1e251b0d35c5d01352eeac2d855 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Sat, 25 Apr 2026 18:17:50 -0700 Subject: [PATCH 3/3] chore(porch): 700 pr phase-transition --- codev/projects/700-dashboard-backlog-show-assigne/status.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codev/projects/700-dashboard-backlog-show-assigne/status.yaml b/codev/projects/700-dashboard-backlog-show-assigne/status.yaml index 8b8f3c13..6a25004f 100644 --- a/codev/projects/700-dashboard-backlog-show-assigne/status.yaml +++ b/codev/projects/700-dashboard-backlog-show-assigne/status.yaml @@ -1,7 +1,7 @@ id: '700' title: dashboard-backlog-show-assigne protocol: air -phase: implement +phase: pr plan_phases: [] current_plan_phase: null gates: @@ -11,4 +11,4 @@ iteration: 1 build_complete: false history: [] started_at: '2026-04-26T01:12:08.135Z' -updated_at: '2026-04-26T01:12:08.136Z' +updated_at: '2026-04-26T01:17:50.460Z'