diff --git a/panel/src/components/dashboard/pr-review-queue.tsx b/panel/src/components/dashboard/pr-review-queue.tsx index cbfcec96..4dce1a1b 100644 --- a/panel/src/components/dashboard/pr-review-queue.tsx +++ b/panel/src/components/dashboard/pr-review-queue.tsx @@ -21,7 +21,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { GitPullRequest, ExternalLink, Rocket, XCircle, FileText } from "lucide-react"; +import { GitPullRequest, ExternalLink, Rocket, XCircle, FileText, Loader2 } from "lucide-react"; import Link from "next/link"; import { type Task } from "@/types"; import { toast } from "sonner"; @@ -133,67 +133,89 @@ export function PrReviewQueue({ className }: PrReviewQueueProps) { - External PRs the org reviewed — supersede or dismiss + External PRs the org is reviewing or has reviewed — the reviewer + posts its change-request on the PR; supersede or dismiss once it + lands
- {items.map((task) => ( -
-
- - {task.title} - - {task.description && ( -

- {task.description} -

- )} -
-
- {task.pr_url && ( - + {items.map((task) => { + const awaiting = (task.status || "").toLowerCase() === "completed"; + return ( +
+ +
+ - - )} - - - - - + + {awaiting && ( + <> + + + + )} +
-
- ))} + ); + })}
diff --git a/roboco/api/routes/tasks.py b/roboco/api/routes/tasks.py index 42c85b9e..65e83698 100644 --- a/roboco/api/routes/tasks.py +++ b/roboco/api/routes/tasks.py @@ -647,10 +647,13 @@ async def get_external_pr_reviews( agent: CurrentAgentContext, permissions: PermissionServiceDep, ) -> list[TaskResponse]: - """Inbound external PRs that were reviewed and await the CEO's decision. + """Inbound external PRs the org is reviewing or has reviewed. - The PR-review decision queue: completed external-PR review tasks the CEO has - neither superseded nor dismissed. Org-wide; visible to PMs and above. + The PR-review queue: external-PR review tasks still in flight (the reviewer + is working) OR completed and awaiting the CEO's decision (not yet superseded + or dismissed). Active reviews surface so the panel shows a review underway + and links to the PR where the change-request is posted, instead of going + dark until it finishes. Org-wide; visible to PMs and above. """ can_view_all = permissions.can_perform_task_action(agent, TaskAction.VIEW_ALL) is_pm = agent.role in (AgentRole.CELL_PM, AgentRole.MAIN_PM) @@ -661,7 +664,7 @@ async def get_external_pr_reviews( detail="Only PMs and management can view the PR-review queue", ) service = get_task_service(db) - tasks = await service.list_external_pr_reviews_awaiting_decision() + tasks = await service.list_external_pr_reviews() return task_list_to_response(tasks) diff --git a/roboco/services/task.py b/roboco/services/task.py index 55ff84ae..7be4eddc 100644 --- a/roboco/services/task.py +++ b/roboco/services/task.py @@ -741,6 +741,37 @@ class TaskService(BaseService): if "dismissed=1" not in (t.quick_context or "").split() ] + async def list_external_pr_reviews(self) -> list[TaskTable]: + """Live external-PR reviews for the panel: in-flight PLUS awaiting-decision. + + Every ``source='external_pr'`` task that is not finished-and-decided: + active reviews (the reviewer is still working — pending / claimed / + in_progress / verifying / blocked / paused) AND completed reviews the CEO + has neither superseded (``confirmed_by_human=True``) nor dismissed + (``dismissed=1`` in quick_context). Cancelled tasks are excluded. + + Active reviews are surfaced on purpose: the reviewer posts its + change-request to the PR itself, so the panel must show that a review is + underway and link to that PR rather than going dark until the review + finishes. Each task carries its ``status`` so the panel can tell + "reviewing" apart from "awaiting your decision". + """ + result = await self.session.execute( + select(TaskTable).where( + TaskTable.source == "external_pr", + TaskTable.status != TaskStatus.CANCELLED, + or_( + TaskTable.status != TaskStatus.COMPLETED, + TaskTable.confirmed_by_human.is_(False), + ), + ) + ) + return [ + t + for t in result.scalars().all() + if "dismissed=1" not in (t.quick_context or "").split() + ] + async def dismiss_external_pr_review(self, task_id: UUID) -> TaskTable | None: """CEO declines to act on a reviewed external PR — drop it from the queue. diff --git a/tests/unit/services/test_external_pr_ingest.py b/tests/unit/services/test_external_pr_ingest.py index 0a282f2d..3966220d 100644 --- a/tests/unit/services/test_external_pr_ingest.py +++ b/tests/unit/services/test_external_pr_ingest.py @@ -76,6 +76,17 @@ async def test_list_awaiting_decision_excludes_dismissed() -> None: assert out == [pending] +@pytest.mark.asyncio +async def test_list_external_pr_reviews_excludes_dismissed() -> None: + # The panel queue surfaces in-flight reviews too (the status filter lives in + # SQL); here we pin the post-query behavior: dismissed reviews drop out. + reviewing = MagicMock(quick_context="external_pr_head=abc") + dismissed = MagicMock(quick_context="external_pr_head=def dismissed=1") + svc = _service([reviewing, dismissed]) + out = await svc.list_external_pr_reviews() + assert out == [reviewing] + + @pytest.mark.asyncio async def test_dismiss_marks_and_is_idempotent() -> None: task = MagicMock(source="external_pr", quick_context="external_pr_head=abc")