diff --git a/ui/src/components/IssueRow.tsx b/ui/src/components/IssueRow.tsx
index d6969d23323..664990b1e17 100644
--- a/ui/src/components/IssueRow.tsx
+++ b/ui/src/components/IssueRow.tsx
@@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import type { Issue } from "@paperclipai/shared";
import { Link } from "@/lib/router";
-import { X } from "lucide-react";
+import { Eye, X } from "lucide-react";
import {
createIssueDetailPath,
rememberIssueDetailLocationState,
@@ -9,6 +9,7 @@ import {
} from "../lib/issueDetailBreadcrumb";
import { cn } from "../lib/utils";
import { StatusIcon } from "./StatusIcon";
+import { productivityReviewTriggerLabel } from "./ProductivityReviewBadge";
type UnreadState = "hidden" | "visible" | "fading";
@@ -63,6 +64,19 @@ export function IssueRow({
const showUnreadDot = unreadState === "visible" || unreadState === "fading";
const selectedStatusClass = selected ? "!text-muted-foreground !border-muted-foreground" : undefined;
const detailState = withIssueDetailHeaderSeed(issueLinkState, issue);
+ const productivityReview = issue.productivityReview ?? null;
+ const productivityReviewIndicator = productivityReview ? (
+
+
+
+ ) : null;
const hasChecklistStep = checklistStepNumber !== null;
const checklistStep = hasChecklistStep ? (
@@ -87,8 +101,9 @@ export function IssueRow({
className,
)}
>
-
+
{mobileLeading ?? }
+ {productivityReviewIndicator}
@@ -105,8 +120,9 @@ export function IssueRow({
) : null}
{desktopMetaLeading ?? (
<>
-
+
+ {productivityReviewIndicator}
{checklistStep}
diff --git a/ui/src/components/ProductivityReviewBadge.tsx b/ui/src/components/ProductivityReviewBadge.tsx
new file mode 100644
index 00000000000..e6c08bfbfe9
--- /dev/null
+++ b/ui/src/components/ProductivityReviewBadge.tsx
@@ -0,0 +1,77 @@
+import { Link } from "react-router-dom";
+import { Eye } from "lucide-react";
+import type { IssueProductivityReview } from "@paperclipai/shared";
+import { cn } from "../lib/utils";
+import { createIssueDetailPath } from "../lib/issueDetailBreadcrumb";
+import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
+
+const TRIGGER_LABELS: Record = {
+ no_comment_streak: "No-comment streak",
+ long_active_duration: "Long active duration",
+ high_churn: "High churn",
+};
+
+const REVIEW_STATUS_LABELS: Record = {
+ todo: "Open",
+ in_progress: "In progress",
+ in_review: "In review",
+ blocked: "Blocked",
+ backlog: "Open",
+};
+
+export function productivityReviewTriggerLabel(
+ trigger: IssueProductivityReview["trigger"],
+): string {
+ if (!trigger) return "Productivity review";
+ return TRIGGER_LABELS[trigger] ?? "Productivity review";
+}
+
+export function ProductivityReviewBadge({
+ review,
+ className,
+ hideLabel = false,
+}: {
+ review: IssueProductivityReview;
+ className?: string;
+ hideLabel?: boolean;
+}) {
+ const label = productivityReviewTriggerLabel(review.trigger);
+ const reviewIdentifier = review.reviewIdentifier ?? review.reviewIssueId.slice(0, 8);
+ const reviewPath = createIssueDetailPath(review.reviewIdentifier ?? review.reviewIssueId);
+ const statusLabel = REVIEW_STATUS_LABELS[review.status] ?? review.status.replace(/_/g, " ");
+
+ return (
+
+
+
+
+ {hideLabel ? null : Productivity review }
+
+
+
+
+
Productivity review open
+
+ Trigger: {label}
+
+ {typeof review.noCommentStreak === "number" && review.noCommentStreak > 0 ? (
+
+ No-comment streak: {" "}
+ {review.noCommentStreak} runs
+
+ ) : null}
+
+ Review: {reviewIdentifier} ({statusLabel})
+
+
+
+
+ );
+}
diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx
index fd207835fd1..8038922a30d 100644
--- a/ui/src/pages/IssueDetail.tsx
+++ b/ui/src/pages/IssueDetail.tsx
@@ -77,6 +77,7 @@ import { ImageGalleryModal } from "../components/ImageGalleryModal";
import { ScrollToBottom } from "../components/ScrollToBottom";
import { StatusIcon } from "../components/StatusIcon";
import { PriorityIcon } from "../components/PriorityIcon";
+import { ProductivityReviewBadge } from "../components/ProductivityReviewBadge";
import { Identity } from "../components/Identity";
import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slots";
import { PluginLauncherOutlet } from "@/plugins/launchers";
@@ -108,6 +109,7 @@ import {
Check,
ChevronRight,
Copy,
+ Eye,
EyeOff,
Hexagon,
ListTree,
@@ -2906,6 +2908,20 @@ export function IssueDetail() {
)}
+ {issue.productivityReview ? (
+
+ ) : null}
+
+ {issue.originKind === "issue_productivity_review" ? (
+
+
+ Productivity review
+
+ ) : null}
+
{issue.projectId ? (
10 runs/comments per hour.",
+ review: {
+ reviewIssueId: "review-issue-3",
+ reviewIdentifier: "PAP-2704",
+ status: "todo",
+ priority: "high",
+ trigger: "high_churn",
+ noCommentStreak: 4,
+ createdAt: new Date("2026-04-28T13:45:00.000Z"),
+ updatedAt: new Date("2026-04-28T13:55:00.000Z"),
+ },
+ },
+];
+
+const productivityReviewIssueRowFixtures = productivityReviewFixtures.map((fixture, index) =>
+ createIssue({
+ id: `issue-productivity-source-${index + 1}`,
+ identifier: `PAP-${2710 + index}`,
+ issueNumber: 2710 + index,
+ title: `Source issue under review · ${fixture.label}`,
+ status: index === 1 ? "in_progress" : "in_progress",
+ priority: fixture.review.priority,
+ productivityReview: fixture.review,
+ lastActivityAt: fixture.review.updatedAt,
+ updatedAt: fixture.review.updatedAt,
+ }),
+);
+
+function ProductivityReviewMatrix() {
+ return (
+
+
+ {productivityReviewFixtures.map((fixture) => (
+
+
+
+
{fixture.label}
+
{fixture.description}
+
+
+
+
+ Trigger {fixture.review.trigger ?? "unknown"} · review {fixture.review.reviewIdentifier}
+
+
+ ))}
+
+
+
+ IssueRow with productivity-review indicator
+
+
+ {productivityReviewIssueRowFixtures.map((issue) => (
+ } />
+ ))}
+
+
+
+ The amber pill links from the source issue header to the open productivity-review child. List rows
+ get a smaller eye glyph next to the status icon so operators can spot yellow tasks without the
+ clickable label. The review issue itself shows the same amber pill in static (non-link) form.
+
+
+ );
+}
+
function StatusLanguage() {
const [priority, setPriority] = useState("high");
@@ -466,6 +577,10 @@ function StatusLanguage() {
+
+