+
{issue.title}
{desktopMetaLeading}
{desktopTrailing}
+ {checklistDependencyChips}
),
}));
@@ -428,6 +445,57 @@ describe("IssuesList", () => {
});
});
+ it("adds checklist affordances for workflow-sorted sub-issue lists", async () => {
+ const issueDone = createIssue({
+ id: "issue-done",
+ identifier: "PAP-1",
+ title: "Done first",
+ status: "done",
+ createdAt: new Date("2026-04-01T00:00:00.000Z"),
+ });
+ const issueBlocked = createIssue({
+ id: "issue-blocked",
+ identifier: "PAP-2",
+ title: "Blocked issue",
+ status: "blocked",
+ blockedBy: [{ id: "issue-active", identifier: "PAP-3", title: "Active blocker", status: "todo", priority: "medium", assigneeAgentId: null, assigneeUserId: null }],
+ createdAt: new Date("2026-04-02T00:00:00.000Z"),
+ });
+ const issueActive = createIssue({
+ id: "issue-active",
+ identifier: "PAP-3",
+ title: "Active blocker",
+ status: "todo",
+ createdAt: new Date("2026-04-03T00:00:00.000Z"),
+ });
+
+ const { root } = renderWithQueryClient(
+
undefined}
+ />,
+ container,
+ );
+
+ await waitForAssertion(() => {
+ const rows = Array.from(container.querySelectorAll('[data-testid="issue-row"]'));
+ expect(rows).toHaveLength(3);
+ expect(rows.map((row) => row.getAttribute("data-step"))).toEqual(["1", "2", "3"]);
+ expect(rows.filter((row) => row.getAttribute("data-current-step") === "true")).toHaveLength(1);
+ expect(rows.find((row) => row.textContent?.includes("Active blocker"))?.getAttribute("data-current-step")).toBe("true");
+ expect(rows.find((row) => row.textContent?.includes("Done first"))?.getAttribute("data-title-class")).toContain("text-muted-foreground");
+ expect(container.textContent).toContain("blocked by PAP-3");
+ });
+
+ act(() => {
+ root.unmount();
+ });
+ });
+
it("hides the sub-issue progress summary unless it is enabled and populated", async () => {
const { root } = renderWithQueryClient(
value?.toLowerCase().includes(normalizedSearch));
}
+function isActionableWorkflowStatus(status: IssueStatus): boolean {
+ return status !== "done" && status !== "cancelled" && status !== "blocked";
+}
+
/* ── Component ── */
interface Agent {
@@ -820,6 +824,42 @@ export function IssuesList({
: null,
[issues, showProgressSummary],
);
+ const checklistAffordanceEnabled = useMemo(
+ () =>
+ defaultSortField === "workflow"
+ && viewState.sortField === "workflow"
+ && viewState.groupBy === "none",
+ [defaultSortField, viewState.groupBy, viewState.sortField],
+ );
+ const workflowChecklistMeta = useMemo(() => {
+ if (!checklistAffordanceEnabled) return null;
+
+ const visibleIssueIds = new Set(filtered.map((issue) => issue.id));
+ const stepNumberByIssueId = new Map();
+ const unresolvedVisibleBlockersByIssueId = new Map();
+
+ filtered.forEach((issue, index) => {
+ stepNumberByIssueId.set(issue.id, index + 1);
+ const unresolvedVisible = (issue.blockedBy ?? [])
+ .map((blocker) => blocker.id)
+ .filter((blockerId) => {
+ if (!visibleIssueIds.has(blockerId)) return false;
+ const blockerIssue = issueById.get(blockerId);
+ if (!blockerIssue) return false;
+ return blockerIssue.status !== "done" && blockerIssue.status !== "cancelled";
+ });
+ unresolvedVisibleBlockersByIssueId.set(issue.id, unresolvedVisible);
+ });
+
+ const firstActionable = filtered.find((issue) => isActionableWorkflowStatus(issue.status)) ?? null;
+ const currentStepIssue = firstActionable ?? filtered.find((issue) => issue.status === "blocked") ?? null;
+
+ return {
+ stepNumberByIssueId,
+ unresolvedVisibleBlockersByIssueId,
+ currentStepIssueId: currentStepIssue?.id ?? null,
+ };
+ }, [checklistAffordanceEnabled, filtered, issueById]);
const { data: labels } = useQuery({
queryKey: queryKeys.issues.labels(selectedCompanyId!),
@@ -1237,6 +1277,41 @@ export function IssuesList({
: viewState.collapsedParents.filter((id) => id !== issue.id),
});
};
+ const checklistMeta = workflowChecklistMeta;
+ const checklistStepNumber = checklistMeta?.stepNumberByIssueId.get(issue.id) ?? null;
+ const unresolvedVisibleBlockers = checklistMeta?.unresolvedVisibleBlockersByIssueId.get(issue.id) ?? [];
+ const checklistRowId = checklistMeta ? `issue-workflow-row-${issue.id}` : undefined;
+ const doneRowTitleClass = checklistMeta && issue.status === "done"
+ ? "text-muted-foreground"
+ : undefined;
+ const checklistDependencyChips = checklistMeta && unresolvedVisibleBlockers.length > 0 ? (
+ <>
+ {unresolvedVisibleBlockers.map((blockerId) => {
+ const blockerIssue = issueById.get(blockerId);
+ if (!blockerIssue) return null;
+ const label = blockerIssue.identifier ?? blockerIssue.id.slice(0, 8);
+ return (
+
+ );
+ })}
+ >
+ ) : null;
return (
{hasChildren && !isExpanded ? (