From 0746664f72461a645823d32bdb99f5d20a64c316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 19 Apr 2026 01:24:01 -0400 Subject: [PATCH] =?UTF-8?q?feat(review-panel):=20split=20reviews=20by=20au?= =?UTF-8?q?thor=20=E2=80=94=20board=20/=20human=20/=20manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One flat list of `status = in_review` tasks couldn't tell the operator whether the next decision belonged to the board (approval pending), to the human user (top-level assignee or explicit `requires_user_review`), or to a middle manager walking up the `reports_to` chain. Three very different escalation paths rendered identically, so "reviewed by X" was the only signal and operators missed the ones actually waiting on them. `PendingReviewList` now classifies each in-review task into one of three kinds (`board` / `user` / `manager`) using the pending approval set plus the existing assignee/manager lookup. Rendering groups them under labelled sub-headings in priority order (board first — the operator is the bottleneck there), each with its own awaiting-text so the row's status is unambiguous at a glance. The component accepts approvals via prop; the caller passes the existing `pendingApprovals` it already holds. Translations for both locales carry the new group labels. --- .../execution/PendingReviewList.tsx | 137 +++++++++++++++--- src/renderer/i18n/en.ts | 5 + src/renderer/i18n/zh.ts | 5 + .../views/sections/ExecutionSections.tsx | 2 +- 4 files changed, 131 insertions(+), 18 deletions(-) diff --git a/src/renderer/components/execution/PendingReviewList.tsx b/src/renderer/components/execution/PendingReviewList.tsx index 6fb58a2b..cad0ebf4 100644 --- a/src/renderer/components/execution/PendingReviewList.tsx +++ b/src/renderer/components/execution/PendingReviewList.tsx @@ -1,27 +1,130 @@ import { Robot } from "../ui/icons" -import type { AgentRecord, TaskRecord } from "@shared/types" -import { useT } from "../../i18n" +import type { AgentRecord, ApprovalRecord, TaskRecord } from "@shared/types" +import { useT, type TranslationFn } from "../../i18n" import { EmptyState, ListFrame } from "../ui" -export function PendingReviewList({ tasks, agents }: { tasks: TaskRecord[]; agents: AgentRecord[] }) { +interface PendingReviewListProps { + tasks: TaskRecord[] + agents: AgentRecord[] + approvals: ApprovalRecord[] +} + +type ReviewKind = "board" | "user" | "manager" + +interface ReviewRow { + task: TaskRecord + assignee: AgentRecord | null + reviewer: AgentRecord | null + kind: ReviewKind +} + +function classifyReview( + task: TaskRecord, + agents: AgentRecord[], + pendingApprovalTaskIds: Set, +): ReviewRow { + const assignee = agents.find((a) => a.id === task.assigneeAgentId) ?? null + if (pendingApprovalTaskIds.has(task.id)) { + return { task, assignee, reviewer: null, kind: "board" } + } + if (task.requiresUserReview) { + return { task, assignee, reviewer: null, kind: "user" } + } + if (!assignee) { + return { task, assignee: null, reviewer: null, kind: "user" } + } + if (!assignee.reportsTo) { + return { task, assignee, reviewer: null, kind: "user" } + } + const reviewer = agents.find((a) => a.id === assignee.reportsTo) ?? null + return { task, assignee, reviewer, kind: "manager" } +} + +function groupTitle(kind: ReviewKind, t: TranslationFn): string { + if (kind === "board") return t("exec.reviewGroupBoard") + if (kind === "user") return t("exec.reviewGroupUser") + return t("exec.reviewGroupManager") +} + +function renderReviewerLine(row: ReviewRow, t: TranslationFn): React.ReactNode { + const assigneeName = row.assignee?.name ?? "Unassigned" + if (row.kind === "board") { + return ( + <> + {assigneeName} · {t("exec.reviewAwaitsBoard")} + + ) + } + if (row.kind === "user") { + return ( + <> + {assigneeName} · {t("exec.reviewAwaitsUser")} + + ) + } + const reviewer = row.reviewer + if (!reviewer) { + return <>{assigneeName} · {t("exec.awaitingReview")} + } + return ( + <> + {assigneeName} → {t("exec.reviewedBy")}{" "} + + {reviewer.name} + {reviewer.title ? ` (${reviewer.title})` : ""} + + + ) +} + +function ReviewGroup({ title, rows, t }: { title: string; rows: ReviewRow[]; t: TranslationFn }) { + return ( +
+
+ {title} +
+ {rows.map((row) => ( +
+ +
+
{row.task.title}
+
{renderReviewerLine(row, t)}
+
+
+ ))} +
+ ) +} + +/** + * Pending reviews separated into three visually distinct groups so + * Board / Human / Manager review semantics do not collapse into one + * faceless list. Priority order (board > user > manager) lines up + * with who is actually being asked to act next. + */ +export function PendingReviewList({ tasks, agents, approvals }: PendingReviewListProps) { const t = useT() const reviewTasks = tasks.filter((tk) => tk.status === "in_review") - if (reviewTasks.length === 0) return + if (reviewTasks.length === 0) { + return + } + const pendingApprovalTaskIds = new Set( + approvals + .filter((approval) => approval.state === "pending" || approval.state === "revision_requested") + .map((approval) => approval.relatedTaskId) + .filter((taskId): taskId is string => Boolean(taskId)), + ) + + const rows = reviewTasks.map((task) => classifyReview(task, agents, pendingApprovalTaskIds)) + const boardRows = rows.filter((r) => r.kind === "board") + const userRows = rows.filter((r) => r.kind === "user") + const managerRows = rows.filter((r) => r.kind === "manager") + return ( - {reviewTasks.map((task) => { - const assignee = agents.find((a) => a.id === task.assigneeAgentId) ?? null - const reviewer = assignee?.reportsTo ? (agents.find((a) => a.id === assignee.reportsTo) ?? null) : null - return ( -
- -
-
{task.title}
-
{assignee ? assignee.name : "Unassigned"}{reviewer ? <> → reviewed by {reviewer.name}{reviewer.title ? ` (${reviewer.title})` : ""} : " · awaiting review"}
-
-
- ) - })} + {boardRows.length > 0 ? : null} + {userRows.length > 0 ? : null} + {managerRows.length > 0 ? : null}
) } diff --git a/src/renderer/i18n/en.ts b/src/renderer/i18n/en.ts index a98a3f39..233081a9 100644 --- a/src/renderer/i18n/en.ts +++ b/src/renderer/i18n/en.ts @@ -461,6 +461,11 @@ export const en: Record = { "exec.noReviewsDetail": "Review items show up here.", "exec.awaitingReview": "awaiting review", "exec.reviewedBy": "reviewed by", + "exec.reviewGroupBoard": "Board review", + "exec.reviewGroupUser": "Human review", + "exec.reviewGroupManager": "Manager review", + "exec.reviewAwaitsBoard": "awaiting board approval", + "exec.reviewAwaitsUser": "awaiting your review", "exec.noApprovals": "No pending approvals", "exec.noApprovalsDetail": "Approvals show up here when agents need you.", "exec.deleteApproval": "Delete approval", diff --git a/src/renderer/i18n/zh.ts b/src/renderer/i18n/zh.ts index be032cdc..bf39cb75 100644 --- a/src/renderer/i18n/zh.ts +++ b/src/renderer/i18n/zh.ts @@ -461,6 +461,11 @@ export const zh: Record = { "exec.noReviewsDetail": "待审核任务会出现在这里。", "exec.awaitingReview": "等待审核", "exec.reviewedBy": "由...审核", + "exec.reviewGroupBoard": "董事会审核", + "exec.reviewGroupUser": "人工审核", + "exec.reviewGroupManager": "主管审核", + "exec.reviewAwaitsBoard": "等待董事会批准", + "exec.reviewAwaitsUser": "等待你的审核", "exec.noApprovals": "暂无待审批项", "exec.noApprovalsDetail": "需要你批准的内容会出现在这里。", "exec.deleteApproval": "删除审批", diff --git a/src/renderer/views/sections/ExecutionSections.tsx b/src/renderer/views/sections/ExecutionSections.tsx index 2ab25dd4..3429b25c 100644 --- a/src/renderer/views/sections/ExecutionSections.tsx +++ b/src/renderer/views/sections/ExecutionSections.tsx @@ -242,7 +242,7 @@ export function ExecutionSections({ section }: ExecutionSectionsProps) { {section === "approvals" ? ( <> - +