import type { KanbanState } from "@/lib/state/slices"; import type { Repository, TaskSessionState, TaskState } from "@/lib/types/http"; import type { TaskStatusSummary } from "@/lib/types/task-status-summary"; import { statusSummaryActiveErrorPreview } from "@/lib/task-status-summary"; import { workflowStepTitle } from "./task-session-sidebar-aggregate"; import type { WipQueueStatus } from "@/lib/kanban/wip-queue"; import { resolveTaskRepositorySlugs } from "@/lib/sidebar/sidebar-task-repositories"; import { taskPRInfoFromSummary } from "./task-pr-info"; import type { SidebarTaskColorAutomation } from "@/lib/task-color-automation-settings"; import { taskColorFacts } from "@/lib/sidebar/task-color-projection"; import { resolveAutomaticTaskColor } from "@/lib/sidebar/task-color-rules"; export type SidebarItemContext = { repositorySlugById: Map; titleById: Map; workflowNameById: Map; stepTitleById: Map; wipQueueByTaskId?: Map; acknowledgedAgentErrors?: Record; dismissedAgentErrors?: Record; workspaceId?: string; repositoriesById?: ReadonlyMap; stepColorById?: ReadonlyMap; automaticColorSettings?: SidebarTaskColorAutomation; pendingArchiveTaskIds?: ReadonlySet; }; const EMPTY_REPOSITORIES_BY_ID = new Map(); const EMPTY_STEP_COLORS = new Map(); function summaryDiffStats( summary: TaskStatusSummary | null | undefined, ): { additions: number; deletions: number } | undefined { const git = summary?.git; if (!git || git.comparison_unavailable) return undefined; const additions = git.additions ?? 0; const deletions = git.deletions ?? 0; return additions > 0 || deletions > 0 ? { additions, deletions } : undefined; } function repositoryPathFromSummary( summary: TaskStatusSummary | null | undefined, ): string | undefined { const url = summary?.pull_request?.url; if (!url) return undefined; try { const path = new URL(url).pathname.split("/").filter(Boolean); if (path.length >= 2) return `${path[0]}/${path[1]}`; } catch { // A malformed provider URL must not make the task switcher disappear. } return undefined; } function pendingFlags(summary: TaskStatusSummary | null | undefined, fallback?: string | null) { const action = summary != null ? summary.pending_action : fallback; return { clarification: action === "clarification", permission: action === "permission", }; } function issueInfoForTask(task: KanbanState["tasks"][number]) { if (!task.issueUrl || !task.issueNumber) return undefined; return { url: task.issueUrl, number: task.issueNumber }; } function sidebarLastActivityAt( summary: TaskStatusSummary | null | undefined, task: KanbanState["tasks"][number], ) { return summary?.last_activity_at ?? task.updatedAt ?? task.createdAt; } function sidebarSessionStatus( summary: TaskStatusSummary | null | undefined, task: KanbanState["tasks"][number], ) { const primarySession = summary?.primary_session; const hasSummary = summary != null; return { sessionState: (hasSummary ? primarySession?.state : task.primarySessionState) as | TaskSessionState | undefined, // The task-level MOST-ACTIVE-WINS activity aggregate (ADR-0049) is // authoritative for the sidebar row: when no status summary is available, // fall back to the task record's own aggregate so multi-session and // off-screen rows still agree with the board card. foregroundActivity: hasSummary ? summary?.foreground_activity : task.foregroundActivity, primarySessionId: hasSummary ? (primarySession?.id ?? null) : (task.primarySessionId ?? null), updatedAt: hasSummary ? summary?.updated_at : (task.updatedAt ?? task.createdAt), lastActivityAt: sidebarLastActivityAt(summary, task), }; } function sidebarStatus( task: KanbanState["tasks"][number] & { _workflowId: string }, context: SidebarItemContext, ) { const summary = task.statusSummary; const pending = pendingFlags(summary, task.taskPendingAction); const sessionStatus = sidebarSessionStatus(summary, task); return { ...sessionStatus, agentErrorMessage: statusSummaryActiveErrorPreview( summary, context.acknowledgedAgentErrors, context.dismissedAgentErrors, ), repositoryPath: repositoryPathFromSummary(summary) ?? (task.repositoryId ? context.repositorySlugById.get(task.repositoryId) : undefined), diffStats: summaryDiffStats(summary), comparisonUnavailable: summary?.git?.comparison_unavailable === true, hasPendingClarification: pending.clarification, hasPendingPermission: pending.permission, prInfo: taskPRInfoFromSummary(summary), issueInfo: issueInfoForTask(task), queuedCount: summary?.queued_prompt_count, launchQueue: summary?.launch_queue, wipQueue: context.wipQueueByTaskId?.get(task.id), }; } function isPendingArchive( task: KanbanState["tasks"][number], context: SidebarItemContext, ): boolean { if (task.isArchived === true) return false; return context.pendingArchiveTaskIds?.has(task.id) === true; } /** Map a task-level status projection to a sidebar item without session streams. */ export function buildSidebarItem( task: KanbanState["tasks"][number] & { _workflowId: string }, context: SidebarItemContext, ) { const status = sidebarStatus(task, context); const facts = taskColorFacts(task, { workspaceId: context.workspaceId, repositoriesById: context.repositoriesById ?? EMPTY_REPOSITORIES_BY_ID, stepColorById: context.stepColorById ?? EMPTY_STEP_COLORS, }); const automaticColor = context.automaticColorSettings ? resolveAutomaticTaskColor(context.automaticColorSettings, facts) : null; return { id: task.id, title: task.title, autopilot: task.autopilot, priority: task.priority, state: task.state as TaskState | undefined, interrupted: task.interrupted, parkedOnBackgroundWork: task.parkedOnBackgroundWork, ...status, description: task.description, workflowId: task._workflowId, workflowName: context.workflowNameById.get(task._workflowId), workflowStepId: task.workflowStepId as string | undefined, workflowStepTitle: workflowStepTitle(task, context.stepTitleById), workspaceId: facts.workspaceId, origin: task.origin, primaryExecutorProfileId: task.primaryExecutorProfileId ?? undefined, workflowStepColor: facts.workflowStepColor, isRemoteExecutor: task.isRemoteExecutor, remoteExecutorId: task.primaryExecutorId ?? undefined, remoteExecutorType: task.primaryExecutorType ?? undefined, remoteExecutorName: task.primaryExecutorName ?? undefined, createdAt: task.createdAt, isArchived: task.isArchived === true, isPendingArchive: isPendingArchive(task, context), isFromOffice: task.isFromOffice, parentTaskTitle: task.parentTaskId ? context.titleById.get(task.parentTaskId) : undefined, parentTaskId: task.parentTaskId ?? undefined, workspaceMode: task.workspaceMode, repositories: resolveTaskRepositorySlugs(task.repositories, context.repositorySlugById), repositoryLinks: task.repositories, repositoryRuleIdentities: facts.repositories, automaticColor: automaticColor?.color, automaticColorSource: automaticColor?.source, isPRReview: task.isPRReview ?? false, isIssueWatch: task.isIssueWatch ?? false, }; }