Files
PaperClipAI/ui/src/lib/issue-hierarchy-context.ts
T
DottaandPaperclip be380ba155 fix(ui): hide agent-created tasks by default (#16157)
Hide agent-created, system-created, and chat-channel tasks from independent collections by default. Preserve task hierarchy context and existing inbox participation. Add a saved opt-in checkbox without helper text.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-13 09:26:37 -05:00

65 lines
2.5 KiB
TypeScript

import type { Issue } from "@paperclipai/shared";
/** Context never promotes an unrelated or orphaned automated task to a result. */
export function includeIssueHierarchyContext(matches: Issue[], context: Issue[]): Issue[] {
const byId = new Map([...context, ...matches].map((issue) => [issue.id, issue]));
const result = new Map(matches.map((issue) => [issue.id, issue]));
for (const match of matches) {
const visited = new Set([match.id]);
let parentId = match.parentId;
while (parentId && !visited.has(parentId)) {
visited.add(parentId);
const parent = byId.get(parentId);
if (!parent) break;
result.set(parent.id, parent);
parentId = parent.parentId;
}
}
// Descendants are rooted at matches, not at contextual ancestors.
const descendantIds = new Set(matches.map((issue) => issue.id));
let changed = true;
while (changed) {
changed = false;
for (const issue of context) {
if (!descendantIds.has(issue.id) && issue.parentId && descendantIds.has(issue.parentId)) {
descendantIds.add(issue.id);
result.set(issue.id, issue);
changed = true;
}
}
}
return [...result.values()];
}
/** Fetch each parent once and stop at the collection's current rendering budget. */
export async function loadIssueHierarchyChildren(
companyId: string,
matches: Issue[],
collapsedParents: string[],
rowBudget: number,
readChildren: (parentId: string, limit: number, offset: number) => Promise<Issue[]>,
): Promise<{ issues: Issue[]; hasMore: boolean }> {
const context: Issue[] = [];
const queue = matches.map((issue) => ({ issue, offset: 0 }));
const visited = new Set<string>();
const fetched = new Set<string>();
let requests = 0;
while (queue.length > 0 && context.length < rowBudget && requests < rowBudget) {
const { issue, offset } = queue.shift()!;
if (offset === 0 && visited.has(issue.id)) continue;
visited.add(issue.id);
const collapsed = collapsedParents.includes(issue.id);
const limit = collapsed ? 1 : Math.min(100, rowBudget - context.length);
const rows = await readChildren(issue.id, limit, offset);
requests += 1;
for (const child of rows) {
if (child.companyId !== companyId || fetched.has(child.id)) continue;
fetched.add(child.id);
context.push(child);
if (!collapsed) queue.push({ issue: child, offset: 0 });
}
if (!collapsed && rows.length === limit) queue.unshift({ issue, offset: offset + rows.length });
}
return { issues: context, hasMore: queue.length > 0 };
}