From 8cdb65febb2c8c65ba178168404b34890dc50dee Mon Sep 17 00:00:00 2001 From: Darren Davison Date: Sat, 4 Apr 2026 02:32:08 +0100 Subject: [PATCH 1/9] feat: show sub-tasks indented under parent in issue list with collapse/expand Sub-tasks are now grouped under their parent issue in the list view. Parent issues with children show a chevron to collapse/expand their subtasks. Child issues are visually indented to indicate hierarchy. Co-Authored-By: Paperclip --- ui/src/components/IssuesList.tsx | 502 ++++++++++++++++--------------- 1 file changed, 256 insertions(+), 246 deletions(-) diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 952c649a..6d56704d 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -219,6 +219,7 @@ export function IssuesList({ return getViewState(scopedKey); }); const [assigneePickerIssueId, setAssigneePickerIssueId] = useState(null); + const [collapsedParents, setCollapsedParents] = useState>(new Set()); const [assigneeSearch, setAssigneeSearch] = useState(""); const [issueSearch, setIssueSearch] = useState(initialSearch ?? ""); const deferredIssueSearch = useDeferredValue(issueSearch); @@ -320,251 +321,6 @@ export function IssuesList({ setAssigneeSearch(""); }, [onUpdateIssue]); - const listContent = useMemo(() => { - if (viewState.viewMode === "board") { - return ( - - ); - } - - return groupedContent.map((group) => ( - { - updateView({ - collapsedGroups: open - ? viewState.collapsedGroups.filter((k) => k !== group.key) - : [...viewState.collapsedGroups, group.key], - }); - }} - > - {group.label && ( -
- - - - {group.label} - - - -
- )} - - {group.items.map((issue) => ( - { - e.preventDefault(); - e.stopPropagation(); - }} - > - onUpdateIssue(issue.id, { status: s })} - /> - - )} - desktopMetaLeading={( - <> - { - e.preventDefault(); - e.stopPropagation(); - }} - > - onUpdateIssue(issue.id, { status: s })} - /> - - - {issue.identifier ?? issue.id.slice(0, 8)} - - {liveIssueIds?.has(issue.id) && ( - - - - - - - Live - - - )} - - )} - mobileMeta={timeAgo(issue.updatedAt)} - desktopTrailing={( - <> - {(issue.labels ?? []).length > 0 && ( - - {(issue.labels ?? []).slice(0, 3).map((label) => ( - - {label.name} - - ))} - {(issue.labels ?? []).length > 3 && ( - - +{(issue.labels ?? []).length - 3} - - )} - - )} - { - setAssigneePickerIssueId(open ? issue.id : null); - if (!open) setAssigneeSearch(""); - }} - > - - - - e.stopPropagation()} - onPointerDownOutside={() => setAssigneeSearch("")} - > - setAssigneeSearch(e.target.value)} - autoFocus - /> -
- - {currentUserId && ( - - )} - {(agents ?? []) - .filter((agent) => { - if (!assigneeSearch.trim()) return true; - return agent.name - .toLowerCase() - .includes(assigneeSearch.toLowerCase()); - }) - .map((agent) => ( - - ))} -
-
-
- - )} - trailingMeta={formatDate(issue.createdAt)} - /> - ))} -
-
- )); - }, [ - agents, - agentName, - assigneePickerIssueId, - assigneeSearch, - assignIssue, - currentUserId, - filtered, - groupedContent, - issueLinkState, - liveIssueIds, - newIssueDefaults, - onUpdateIssue, - openNewIssue, - updateView, - viewState.collapsedGroups, - ]); return (
@@ -870,7 +626,261 @@ export function IssuesList({ /> )} - {listContent} + {viewState.viewMode === "board" ? ( + + ) : ( + groupedContent.map((group) => ( + { + updateView({ + collapsedGroups: open + ? viewState.collapsedGroups.filter((k) => k !== group.key) + : [...viewState.collapsedGroups, group.key], + }); + }} + > + {group.label && ( +
+ + + + {group.label} + + + +
+ )} + + {(() => { + const itemIds = new Set(group.items.map((i) => i.id)); + const roots = group.items.filter((i) => !i.parentId || !itemIds.has(i.parentId)); + const childMap = new Map(); + for (const item of group.items) { + if (item.parentId && itemIds.has(item.parentId)) { + const arr = childMap.get(item.parentId) ?? []; + arr.push(item); + childMap.set(item.parentId, arr); + } + } + + const renderIssueRow = (issue: Issue, isChild: boolean) => { + const children = childMap.get(issue.id) ?? []; + const hasChildren = children.length > 0; + const isExpanded = !collapsedParents.has(issue.id); + const toggleCollapse = (e: { preventDefault: () => void; stopPropagation: () => void }) => { + e.preventDefault(); + e.stopPropagation(); + setCollapsedParents((prev) => { + const next = new Set(prev); + if (next.has(issue.id)) next.delete(issue.id); else next.add(issue.id); + return next; + }); + }; + + return ( +
+ + + + ) : ( + { e.preventDefault(); e.stopPropagation(); }}> + onUpdateIssue(issue.id, { status: s })} /> + + ) + } + desktopMetaLeading={( + <> + {hasChildren ? ( + + ) : ( + + )} + { e.preventDefault(); e.stopPropagation(); }} + > + onUpdateIssue(issue.id, { status: s })} /> + + + {issue.identifier ?? issue.id.slice(0, 8)} + + {liveIssueIds?.has(issue.id) && ( + + + + + + + Live + + + )} + + )} + mobileMeta={timeAgo(issue.updatedAt)} + desktopTrailing={( + <> + {(issue.labels ?? []).length > 0 && ( + + {(issue.labels ?? []).slice(0, 3).map((label) => ( + + {label.name} + + ))} + {(issue.labels ?? []).length > 3 && ( + + +{(issue.labels ?? []).length - 3} + + )} + + )} + { + setAssigneePickerIssueId(open ? issue.id : null); + if (!open) setAssigneeSearch(""); + }} + > + + + + e.stopPropagation()} + onPointerDownOutside={() => setAssigneeSearch("")} + > + setAssigneeSearch(e.target.value)} + autoFocus + /> +
+ + {currentUserId && ( + + )} + {(agents ?? []) + .filter((agent) => { + if (!assigneeSearch.trim()) return true; + return agent.name.toLowerCase().includes(assigneeSearch.toLowerCase()); + }) + .map((agent) => ( + + ))} +
+
+
+ + )} + trailingMeta={formatDate(issue.createdAt)} + /> + {hasChildren && isExpanded && children.map((child) => renderIssueRow(child, true))} +
+ ); + }; + + return roots.map((issue) => renderIssueRow(issue, false)); + })()} +
+
+ )) + )}
); } From 11643941e69a3b6b47ec6ae43fab17f2fd17c7b4 Mon Sep 17 00:00:00 2001 From: Darren Davison Date: Sat, 4 Apr 2026 02:39:04 +0100 Subject: [PATCH 2/9] fix: add sm:pl-7 to ensure child indentation is visible on desktop The base IssueRow has sm:pl-1 which overrides pl-6 at sm+ breakpoints. Adding sm:pl-7 ensures the indent is visible at all screen sizes. Co-Authored-By: Paperclip --- ui/src/components/IssuesList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 6d56704d..948feeac 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -696,7 +696,7 @@ export function IssuesList({ From 12011fa9deb2694f227ced37b19b01582b330031 Mon Sep 17 00:00:00 2001 From: Darren Davison Date: Sat, 4 Apr 2026 02:46:02 +0100 Subject: [PATCH 3/9] feat: show sub-task count in title when parent is collapsed When a parent issue is collapsed, its title is suffixed with "(N sub-tasks)" so the count remains visible at a glance. The suffix disappears when the parent is expanded. Co-Authored-By: Paperclip --- ui/src/components/IssueRow.tsx | 4 +++- ui/src/components/IssuesList.tsx | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ui/src/components/IssueRow.tsx b/ui/src/components/IssueRow.tsx index 8a01e585..5711a69c 100644 --- a/ui/src/components/IssueRow.tsx +++ b/ui/src/components/IssueRow.tsx @@ -18,6 +18,7 @@ interface IssueRowProps { mobileMeta?: ReactNode; desktopTrailing?: ReactNode; trailingMeta?: ReactNode; + titleSuffix?: ReactNode; unreadState?: UnreadState | null; onMarkRead?: () => void; onArchive?: () => void; @@ -35,6 +36,7 @@ export function IssueRow({ mobileMeta, desktopTrailing, trailingMeta, + titleSuffix, unreadState = null, onMarkRead, onArchive, @@ -63,7 +65,7 @@ export function IssueRow({ - {issue.title} + {issue.title}{titleSuffix} {desktopLeadingSpacer ? ( diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 948feeac..1e731e53 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -697,6 +697,11 @@ export function IssuesList({ issue={issue} issueLinkState={issueLinkState} className={isChild ? "pl-6 sm:pl-7" : undefined} + titleSuffix={hasChildren && !isExpanded ? ( + + ({children.length} sub-task{children.length !== 1 ? "s" : ""}) + + ) : undefined} mobileLeading={ hasChildren ? (