import type { IssueBlockerAttentionIssueSummary, IssueRelationIssueSummary, } from "@paperclipai/shared"; import { CheckCircle2, Circle } from "lucide-react"; import { createIssueDetailPath } from "@/lib/issueDetailBreadcrumb"; import { orderWaitingBlockers, type WaitingBlockerStatus, type WaitingBlockerStep, } from "@/lib/issue-blockers"; import { Link } from "@/lib/router"; import { useStreamlinedTaskChatPresentation } from "./presentation-mode"; function isUnresolved(blocker: IssueRelationIssueSummary): boolean { return blocker.status !== "done" && blocker.status !== "cancelled"; } export function resolveTaskChatBlockers( blockers: IssueRelationIssueSummary[], terminalBlockerIssueId?: string | null, directBlockerIssueId?: string | null, terminalBlocker?: IssueBlockerAttentionIssueSummary | null, ): { directBlocker: IssueRelationIssueSummary | IssueBlockerAttentionIssueSummary; ultimateBlocker: IssueRelationIssueSummary | IssueBlockerAttentionIssueSummary | null; } | null { const unresolvedBlockers = blockers.filter(isUnresolved); if (unresolvedBlockers.length === 0) return null; const directBlocker = directBlockerIssueId ? unresolvedBlockers.find((blocker) => blocker.id === directBlockerIssueId) : terminalBlockerIssueId ? unresolvedBlockers.find((blocker) => ( blocker.id === terminalBlockerIssueId || blocker.terminalBlockers?.some((terminal) => terminal.id === terminalBlockerIssueId) )) : unresolvedBlockers[0]; // A selected intermediate blocker is not part of `terminalBlockers`, which // intentionally contains only structural leaves. If its direct path is not // in this payload (for example a child-derived attention path), show the // selected task itself instead of falling back to an unrelated blocker. if (!directBlocker) { if (!terminalBlocker) return null; return { directBlocker: terminalBlocker, ultimateBlocker: null, }; } const terminalBlockers = directBlocker.terminalBlockers?.filter(isUnresolved) ?? []; const ultimateBlocker = terminalBlockerIssueId ? terminalBlocker?.id === terminalBlockerIssueId ? terminalBlocker : terminalBlockers.find((blocker) => blocker.id === terminalBlockerIssueId) ?? null : terminalBlockers[0] ?? null; return { directBlocker, ultimateBlocker: ultimateBlocker?.id === directBlocker.id ? null : ultimateBlocker, }; } export interface ResolvedTaskChatLiveWork { steps: WaitingBlockerStep[]; nowRunning: Array; } export function resolveTaskChatLiveWork( blockers: IssueRelationIssueSummary[], liveIssueIds: ReadonlySet, selectedTerminalBlocker?: IssueBlockerAttentionIssueSummary | null, ): ResolvedTaskChatLiveWork | null { if (blockers.length === 0) return null; const steps = orderWaitingBlockers(blockers, liveIssueIds); const stepIds = new Set(steps.map((step) => step.blocker.id)); const terminalCandidates: Array = []; for (const blocker of blockers) terminalCandidates.push(...(blocker.terminalBlockers ?? [])); if (selectedTerminalBlocker) terminalCandidates.push(selectedTerminalBlocker); const nowRunning: Array = []; const seen = new Set(); for (const blocker of terminalCandidates) { if (!liveIssueIds.has(blocker.id) || stepIds.has(blocker.id) || seen.has(blocker.id)) continue; seen.add(blocker.id); nowRunning.push(blocker); } const hasLiveStep = steps.some((step) => step.status === "running"); if (!hasLiveStep && nowRunning.length === 0) return null; return { steps, nowRunning }; } function BlockerRow({ label, blocker, }: { label: string; blocker: IssueRelationIssueSummary | IssueBlockerAttentionIssueSummary; }) { const streamlined = useStreamlinedTaskChatPresentation(); const issuePathId = blocker.identifier ?? blocker.id; return streamlined ? ( {label} {blocker.identifier ?? blocker.id.slice(0, 8)} {blocker.title} ) : (
{label} {blocker.identifier ?? blocker.id.slice(0, 8)} {blocker.title}
); } function LiveWorkGlyph({ status }: { status: WaitingBlockerStatus }) { const label = status === "done" ? "Done" : status === "running" ? "Running" : "Waiting"; if (status === "done") { return ( ); } if (status === "running") { return ( ); } return ( ); } function LiveWorkLink({ blocker, status, label, }: { blocker: IssueRelationIssueSummary | IssueBlockerAttentionIssueSummary; status: WaitingBlockerStatus; label?: string; }) { const streamlined = useStreamlinedTaskChatPresentation(); const issuePathId = blocker.identifier ?? blocker.id; return ( {streamlined && label ? {label} : null} {streamlined ? : null} {blocker.identifier ?? blocker.id.slice(0, 8)} {blocker.title} ); } export function TaskChatBlockerLinks({ directBlocker, ultimateBlocker, placement, }: { directBlocker: IssueRelationIssueSummary | IssueBlockerAttentionIssueSummary; ultimateBlocker: IssueRelationIssueSummary | IssueBlockerAttentionIssueSummary | null; placement: "top" | "bottom"; }) { const streamlined = useStreamlinedTaskChatPresentation(); const directLabel = streamlined && placement === "bottom" ? "Still blocked by" : "Blocked by"; const rootLabel = streamlined ? placement === "bottom" ? "Root blocker remains" : "Root blocker" : "Ultimately blocked by"; return (
{ultimateBlocker ? ( ) : null}
); } export function TaskChatLiveWorkLinks({ liveWork, placement, }: { liveWork: ResolvedTaskChatLiveWork; placement: "top" | "bottom"; }) { const streamlined = useStreamlinedTaskChatPresentation(); const heading = streamlined && placement === "bottom" ? "Still waiting on live work" : "Waiting on live work"; return (
{heading}
    {liveWork.steps.map(({ blocker, status }, index) => (
  1. {!streamlined ? ( <> {index + 1}. ) : null}
  2. ))}
{liveWork.nowRunning.map((blocker) => streamlined ? ( ) : (
Now running
))}
); }