import type { Approval, DashboardSummary, HeartbeatRun, InboxDismissal, Issue, JoinRequest, } from "@paperclipai/shared"; import { applyIssueFilters, defaultIssueFilterState, normalizeIssueFilterState, type IssueFilterState, } from "./issue-filters"; import { formatAssigneeUserLabel } from "./assignees"; export const RECENT_ISSUES_LIMIT = 100; export const FAILED_RUN_STATUSES = new Set(["failed", "timed_out"]); export const ACTIONABLE_APPROVAL_STATUSES = new Set(["pending", "revision_requested"]); export const DISMISSED_KEY = "paperclip:inbox:dismissed"; export const READ_ITEMS_KEY = "paperclip:inbox:read-items"; export const INBOX_LAST_TAB_KEY = "paperclip:inbox:last-tab"; export const INBOX_ISSUE_COLUMNS_KEY = "paperclip:inbox:issue-columns"; export const INBOX_NESTING_KEY = "paperclip:inbox:nesting"; export const INBOX_GROUP_BY_KEY = "paperclip:inbox:group-by"; export const INBOX_FILTER_PREFERENCES_KEY_PREFIX = "paperclip:inbox:filters"; export const INBOX_COLLAPSED_GROUPS_KEY_PREFIX = "paperclip:inbox:collapsed-groups"; export type InboxTab = "mine" | "recent" | "unread" | "blocked" | "all"; export type InboxCategoryFilter = | "everything" | "issues_i_touched" | "join_requests" | "approvals" | "failed_runs" | "alerts"; export type InboxApprovalFilter = "all" | "actionable" | "resolved"; export type InboxWorkItemGroupBy = "none" | "type" | "assignee" | "project" | "workspace"; export const inboxIssueColumns = [ "status", "id", "assignee", "project", "workspace", "parent", "labels", "updated", ] as const; export type InboxIssueColumn = (typeof inboxIssueColumns)[number]; export const DEFAULT_INBOX_ISSUE_COLUMNS: InboxIssueColumn[] = ["status", "id", "updated"]; export interface InboxFilterPreferences { allCategoryFilter: InboxCategoryFilter; allApprovalFilter: InboxApprovalFilter; issueFilters: IssueFilterState; } export type InboxWorkItem = | { kind: "issue"; timestamp: number; issue: Issue; } | { kind: "approval"; timestamp: number; approval: Approval; } | { kind: "failed_run"; timestamp: number; run: HeartbeatRun; } | { kind: "join_request"; timestamp: number; joinRequest: JoinRequest; }; export interface InboxBadgeData { inbox: number; approvals: number; failedRuns: number; joinRequests: number; mineIssues: number; alerts: number; } export interface InboxWorkItemGroup { key: string; label: string | null; items: InboxWorkItem[]; } export type InboxSearchSection = "none" | "archived" | "other"; export interface InboxGroupedSection { key: string; label: string | null; displayItems: InboxWorkItem[]; childrenByIssueId: Map; searchSection: InboxSearchSection; } export interface InboxKeyboardGroupSection { key: string; label?: string | null; displayItems: InboxWorkItem[]; childrenByIssueId: ReadonlyMap; } export type InboxKeyboardNavEntry = | { type: "group"; groupKey: string; label: string; collapsed: boolean; } | { type: "top"; itemKey: string; item: InboxWorkItem; } | { type: "child"; issueId: string; issue: Issue; }; export interface InboxProjectWorkspaceLookup { name: string; projectId?: string | null; } export interface InboxExecutionWorkspaceLookup { name: string; mode: "shared_workspace" | "isolated_workspace" | "operator_branch" | "adapter_managed" | "cloud_sandbox"; projectWorkspaceId: string | null; projectId?: string | null; } export interface InboxWorkspaceGroupingOptions { executionWorkspaceById?: ReadonlyMap; projectWorkspaceById?: ReadonlyMap; defaultProjectWorkspaceIdByProjectId?: ReadonlyMap; projectById?: ReadonlyMap; agentById?: ReadonlyMap; userLabelById?: ReadonlyMap; currentUserId?: string | null; } export interface InboxIssueGroupCreateDefaults { projectId?: string; projectWorkspaceId?: string; executionWorkspaceId?: string; executionWorkspaceMode?: string; assigneeAgentId?: string; assigneeUserId?: string; } const defaultInboxFilterPreferences: InboxFilterPreferences = { allCategoryFilter: "everything", allApprovalFilter: "all", issueFilters: defaultIssueFilterState, }; function normalizeInboxCategoryFilter(value: unknown): InboxCategoryFilter { return value === "issues_i_touched" || value === "join_requests" || value === "approvals" || value === "failed_runs" || value === "alerts" ? value : "everything"; } function normalizeInboxApprovalFilter(value: unknown): InboxApprovalFilter { return value === "actionable" || value === "resolved" ? value : "all"; } function getInboxFilterPreferencesStorageKey(companyId: string | null | undefined): string | null { if (!companyId) return null; return `${INBOX_FILTER_PREFERENCES_KEY_PREFIX}:${companyId}`; } function getInboxCollapsedGroupsStorageKey(companyId: string | null | undefined): string | null { if (!companyId) return null; return `${INBOX_COLLAPSED_GROUPS_KEY_PREFIX}:${companyId}`; } export function loadInboxFilterPreferences( companyId: string | null | undefined, ): InboxFilterPreferences { const storageKey = getInboxFilterPreferencesStorageKey(companyId); if (!storageKey) { return { ...defaultInboxFilterPreferences, issueFilters: { ...defaultIssueFilterState }, }; } try { const raw = localStorage.getItem(storageKey); if (!raw) { return { ...defaultInboxFilterPreferences, issueFilters: { ...defaultIssueFilterState }, }; } const parsed = JSON.parse(raw) as Record; return { allCategoryFilter: normalizeInboxCategoryFilter(parsed.allCategoryFilter), allApprovalFilter: normalizeInboxApprovalFilter(parsed.allApprovalFilter), issueFilters: normalizeIssueFilterState(parsed.issueFilters), }; } catch { return { ...defaultInboxFilterPreferences, issueFilters: { ...defaultIssueFilterState }, }; } } export function saveInboxFilterPreferences( companyId: string | null | undefined, preferences: InboxFilterPreferences, ) { const storageKey = getInboxFilterPreferencesStorageKey(companyId); if (!storageKey) return; try { localStorage.setItem( storageKey, JSON.stringify({ allCategoryFilter: normalizeInboxCategoryFilter(preferences.allCategoryFilter), allApprovalFilter: normalizeInboxApprovalFilter(preferences.allApprovalFilter), issueFilters: normalizeIssueFilterState(preferences.issueFilters), }), ); } catch { // Ignore localStorage failures. } } export function loadCollapsedInboxGroupKeys( companyId: string | null | undefined, ): Set { const storageKey = getInboxCollapsedGroupsStorageKey(companyId); if (!storageKey) return new Set(); try { const raw = localStorage.getItem(storageKey); if (!raw) return new Set(); const parsed = JSON.parse(raw); return new Set(Array.isArray(parsed) ? parsed.filter((entry): entry is string => typeof entry === "string") : []); } catch { return new Set(); } } export function saveCollapsedInboxGroupKeys( companyId: string | null | undefined, groupKeys: ReadonlySet, ) { const storageKey = getInboxCollapsedGroupsStorageKey(companyId); if (!storageKey) return; try { localStorage.setItem(storageKey, JSON.stringify([...groupKeys])); } catch { // Ignore localStorage failures. } } export function loadDismissedInboxAlerts(): Set { try { const raw = localStorage.getItem(DISMISSED_KEY); if (!raw) return new Set(); const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return new Set(); return new Set(parsed.filter((value): value is string => typeof value === "string" && value.startsWith("alert:"))); } catch { return new Set(); } } export function saveDismissedInboxAlerts(ids: Set) { try { localStorage.setItem(DISMISSED_KEY, JSON.stringify([...ids])); } catch { // Ignore localStorage failures. } } export function buildInboxDismissedAtByKey(dismissals: InboxDismissal[]): Map { return new Map( dismissals.map((dismissal) => [dismissal.itemKey, normalizeTimestamp(dismissal.dismissedAt)]), ); } export function isInboxEntityDismissed( dismissedAtByKey: ReadonlyMap, itemKey: string, activityAt: string | Date | null | undefined, ): boolean { const dismissedAt = dismissedAtByKey.get(itemKey); if (dismissedAt == null) return false; return dismissedAt >= normalizeTimestamp(activityAt); } export function loadReadInboxItems(): Set { try { const raw = localStorage.getItem(READ_ITEMS_KEY); return raw ? new Set(JSON.parse(raw)) : new Set(); } catch { return new Set(); } } export function saveReadInboxItems(ids: Set) { try { localStorage.setItem(READ_ITEMS_KEY, JSON.stringify([...ids])); } catch { // Ignore localStorage failures. } } export function normalizeInboxIssueColumns(columns: Iterable): InboxIssueColumn[] { const selected = new Set(columns); return inboxIssueColumns.filter((column) => selected.has(column)); } export function getAvailableInboxIssueColumns(enableWorkspaceColumn: boolean): InboxIssueColumn[] { if (enableWorkspaceColumn) return [...inboxIssueColumns]; return inboxIssueColumns.filter((column) => column !== "workspace"); } export function loadInboxIssueColumns(): InboxIssueColumn[] { try { const raw = localStorage.getItem(INBOX_ISSUE_COLUMNS_KEY); if (raw === null) return DEFAULT_INBOX_ISSUE_COLUMNS; const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return DEFAULT_INBOX_ISSUE_COLUMNS; return normalizeInboxIssueColumns(parsed); } catch { return DEFAULT_INBOX_ISSUE_COLUMNS; } } export function saveInboxIssueColumns(columns: InboxIssueColumn[]) { try { localStorage.setItem( INBOX_ISSUE_COLUMNS_KEY, JSON.stringify(normalizeInboxIssueColumns(columns)), ); } catch { // Ignore localStorage failures. } } export function loadInboxWorkItemGroupBy(): InboxWorkItemGroupBy { try { const raw = localStorage.getItem(INBOX_GROUP_BY_KEY); return raw === "type" || raw === "assignee" || raw === "project" || raw === "workspace" ? raw : "none"; } catch { return "none"; } } export function saveInboxWorkItemGroupBy(groupBy: InboxWorkItemGroupBy) { try { localStorage.setItem(INBOX_GROUP_BY_KEY, groupBy); } catch { // Ignore localStorage failures. } } export function shouldResetInboxWorkspaceGrouping( groupBy: InboxWorkItemGroupBy, isolatedWorkspacesEnabled: boolean, experimentalSettingsLoaded: boolean, ): boolean { return experimentalSettingsLoaded && groupBy === "workspace" && !isolatedWorkspacesEnabled; } export function shouldIncludeRoutineExecutionIssue( issue: Pick, hideRoutineExecutions: boolean, ): boolean { return !hideRoutineExecutions || issue.originKind !== "routine_execution"; } export function filterInboxIssues(issues: Issue[], hideRoutineExecutions: boolean): Issue[] { if (!hideRoutineExecutions) return issues; return issues.filter((issue) => shouldIncludeRoutineExecutionIssue(issue, hideRoutineExecutions)); } export function matchesInboxIssueSearch( issue: Pick, query: string, { isolatedWorkspacesEnabled = false, executionWorkspaceById, projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, }: InboxWorkspaceGroupingOptions & { isolatedWorkspacesEnabled?: boolean; } = {}, ): boolean { const normalizedQuery = query.trim().toLowerCase(); if (!normalizedQuery) return true; if (issue.title.toLowerCase().includes(normalizedQuery)) return true; if (issue.identifier?.toLowerCase().includes(normalizedQuery)) return true; if (issue.description?.toLowerCase().includes(normalizedQuery)) return true; if (!isolatedWorkspacesEnabled) return false; const workspaceName = resolveIssueWorkspaceName(issue, { executionWorkspaceById, projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, }); return workspaceName?.toLowerCase().includes(normalizedQuery) ?? false; } export function getArchivedInboxSearchIssues({ visibleIssues, searchableIssues, query, isolatedWorkspacesEnabled = false, executionWorkspaceById, projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, }: { visibleIssues: Issue[]; searchableIssues: Issue[]; query: string; isolatedWorkspacesEnabled?: boolean; executionWorkspaceById?: ReadonlyMap; projectWorkspaceById?: ReadonlyMap; defaultProjectWorkspaceIdByProjectId?: ReadonlyMap; }): Issue[] { const normalizedQuery = query.trim(); if (!normalizedQuery) return []; const visibleIssueIds = new Set(visibleIssues.map((issue) => issue.id)); return searchableIssues .filter((issue) => !visibleIssueIds.has(issue.id)) .filter((issue) => matchesInboxIssueSearch(issue, normalizedQuery, { isolatedWorkspacesEnabled, executionWorkspaceById, projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, }), ) .sort(sortIssuesByMostRecentActivity); } export function getInboxSearchSupplementIssues({ query, filteredWorkItems, archivedSearchIssues, remoteIssues, issueFilters, currentUserId, enableRoutineVisibilityFilter = false, liveIssueIds, }: { query: string; filteredWorkItems: InboxWorkItem[]; archivedSearchIssues: Issue[]; remoteIssues: Issue[]; issueFilters: IssueFilterState; currentUserId?: string | null; enableRoutineVisibilityFilter?: boolean; liveIssueIds?: ReadonlySet; }): Issue[] { const normalizedQuery = query.trim(); if (!normalizedQuery) return []; const visibleIssueIds = new Set([ ...filteredWorkItems .filter((item): item is Extract => item.kind === "issue") .map((item) => item.issue.id), ...archivedSearchIssues.map((issue) => issue.id), ]); return applyIssueFilters(remoteIssues, issueFilters, currentUserId, enableRoutineVisibilityFilter, liveIssueIds) .filter((issue) => !visibleIssueIds.has(issue.id)); } function formatDefaultWorkspaceGroupLabel(name: string | null | undefined): string { const normalizedName = name?.trim(); return normalizedName ? `${normalizedName} (default)` : "Default workspace"; } function resolveDefaultProjectWorkspaceInfo( issue: Pick, { projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, }: Pick, ): { id: string; label: string } | null { if (!issue.projectId) return null; const defaultProjectWorkspaceId = defaultProjectWorkspaceIdByProjectId?.get(issue.projectId) ?? null; if (!defaultProjectWorkspaceId) return null; return { id: defaultProjectWorkspaceId, label: formatDefaultWorkspaceGroupLabel(projectWorkspaceById?.get(defaultProjectWorkspaceId)?.name), }; } export function resolveIssueWorkspaceName( issue: Pick, { executionWorkspaceById, projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, }: InboxWorkspaceGroupingOptions, ): string | null { const defaultProjectWorkspaceId = issue.projectId ? defaultProjectWorkspaceIdByProjectId?.get(issue.projectId) ?? null : null; if (issue.executionWorkspaceId) { const executionWorkspace = executionWorkspaceById?.get(issue.executionWorkspaceId) ?? null; const linkedProjectWorkspaceId = executionWorkspace?.projectWorkspaceId ?? issue.projectWorkspaceId ?? null; const isDefaultSharedExecutionWorkspace = executionWorkspace?.mode === "shared_workspace" && linkedProjectWorkspaceId === defaultProjectWorkspaceId; if (isDefaultSharedExecutionWorkspace) return null; const workspaceName = executionWorkspace?.name; if (workspaceName) return workspaceName; } if (issue.projectWorkspaceId) { if (issue.projectWorkspaceId === defaultProjectWorkspaceId) return null; const workspaceName = projectWorkspaceById?.get(issue.projectWorkspaceId)?.name; if (workspaceName) return workspaceName; } return null; } export function resolveIssueWorkspaceGroup( issue: Pick, { executionWorkspaceById, projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, }: InboxWorkspaceGroupingOptions = {}, ): { key: string; label: string } { const defaultProjectWorkspace = resolveDefaultProjectWorkspaceInfo(issue, { projectWorkspaceById, defaultProjectWorkspaceIdByProjectId, }); if (issue.executionWorkspaceId) { const executionWorkspace = executionWorkspaceById?.get(issue.executionWorkspaceId) ?? null; const linkedProjectWorkspaceId = executionWorkspace?.projectWorkspaceId ?? issue.projectWorkspaceId ?? null; const isDefaultSharedExecutionWorkspace = executionWorkspace?.mode === "shared_workspace" && linkedProjectWorkspaceId != null && linkedProjectWorkspaceId === defaultProjectWorkspace?.id; if (isDefaultSharedExecutionWorkspace && defaultProjectWorkspace) { return { key: `workspace:project:${defaultProjectWorkspace.id}`, label: defaultProjectWorkspace.label, }; } const workspaceName = executionWorkspace?.name?.trim(); if (workspaceName) { return { key: `workspace:execution:${issue.executionWorkspaceId}`, label: workspaceName, }; } } if (issue.projectWorkspaceId) { if (issue.projectWorkspaceId === defaultProjectWorkspace?.id) { return { key: `workspace:project:${defaultProjectWorkspace.id}`, label: defaultProjectWorkspace.label, }; } const workspaceName = projectWorkspaceById?.get(issue.projectWorkspaceId)?.name?.trim(); if (workspaceName) { return { key: `workspace:project:${issue.projectWorkspaceId}`, label: workspaceName, }; } } if (defaultProjectWorkspace) { return { key: `workspace:project:${defaultProjectWorkspace.id}`, label: defaultProjectWorkspace.label, }; } return { key: "workspace:none", label: "No workspace", }; } export function loadInboxNesting(): boolean { try { const raw = localStorage.getItem(INBOX_NESTING_KEY); return raw !== "false"; } catch { return true; } } export function saveInboxNesting(enabled: boolean) { try { localStorage.setItem(INBOX_NESTING_KEY, String(enabled)); } catch { // Ignore localStorage failures. } } export function resolveInboxNestingEnabled(preferenceEnabled: boolean, isMobile: boolean): boolean { return preferenceEnabled && !isMobile; } export function loadLastInboxTab(): InboxTab { try { const raw = localStorage.getItem(INBOX_LAST_TAB_KEY); if ( raw === "all" || raw === "unread" || raw === "recent" || raw === "mine" || raw === "blocked" ) return raw; if (raw === "new") return "mine"; return "mine"; } catch { return "mine"; } } export function saveLastInboxTab(tab: InboxTab) { try { localStorage.setItem(INBOX_LAST_TAB_KEY, tab); } catch { // Ignore localStorage failures. } } export function isMineInboxTab(tab: InboxTab): boolean { return tab === "mine"; } export function shouldShowCompanyAlerts(tab: InboxTab): boolean { return tab === "all"; } export function resolveInboxSelectionIndex( previousIndex: number, itemCount: number, ): number { if (itemCount === 0) return -1; if (previousIndex < 0) return -1; return Math.min(previousIndex, itemCount - 1); } export function getInboxKeyboardSelectionIndex( previousIndex: number, itemCount: number, direction: "next" | "previous", ): number { if (itemCount === 0) return -1; if (previousIndex < 0) return 0; return direction === "next" ? Math.min(previousIndex + 1, itemCount - 1) : Math.max(previousIndex - 1, 0); } export function getLatestFailedRunsByAgent(runs: HeartbeatRun[]): HeartbeatRun[] { const sorted = [...runs].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), ); const latestByAgent = new Map(); for (const run of sorted) { if (!latestByAgent.has(run.agentId)) { latestByAgent.set(run.agentId, run); } } return Array.from(latestByAgent.values()).filter((run) => FAILED_RUN_STATUSES.has(run.status)); } export function normalizeTimestamp(value: string | Date | null | undefined): number { if (!value) return 0; const timestamp = new Date(value).getTime(); return Number.isFinite(timestamp) ? timestamp : 0; } export function issueLastActivityTimestamp(issue: Issue): number { const lastActivityAt = normalizeTimestamp(issue.lastActivityAt); if (lastActivityAt > 0) return lastActivityAt; const lastExternalCommentAt = normalizeTimestamp(issue.lastExternalCommentAt); if (lastExternalCommentAt > 0) return lastExternalCommentAt; return normalizeTimestamp(issue.updatedAt); } export function sortIssuesByMostRecentActivity(a: Issue, b: Issue): number { const activityDiff = issueLastActivityTimestamp(b) - issueLastActivityTimestamp(a); if (activityDiff !== 0) return activityDiff; return normalizeTimestamp(b.updatedAt) - normalizeTimestamp(a.updatedAt); } export function getRecentTouchedIssues(issues: Issue[]): Issue[] { return [...issues].sort(sortIssuesByMostRecentActivity).slice(0, RECENT_ISSUES_LIMIT); } export function getUnreadTouchedIssues(issues: Issue[]): Issue[] { return issues.filter((issue) => issue.isUnreadForMe); } export function getApprovalsForTab( approvals: Approval[], tab: InboxTab, filter: InboxApprovalFilter, currentUserId?: string | null, ): Approval[] { const sortedApprovals = [...approvals].sort( (a, b) => normalizeTimestamp(b.updatedAt) - normalizeTimestamp(a.updatedAt), ); if (tab === "mine") { return sortedApprovals.filter((approval) => isApprovalVisibleInMine(approval, currentUserId)); } if (tab === "recent") return sortedApprovals; if (tab === "unread") { return sortedApprovals.filter((approval) => ACTIONABLE_APPROVAL_STATUSES.has(approval.status)); } if (filter === "all") return sortedApprovals; return sortedApprovals.filter((approval) => { const isActionable = ACTIONABLE_APPROVAL_STATUSES.has(approval.status); return filter === "actionable" ? isActionable : !isActionable; }); } export function isApprovalVisibleInMine( approval: Approval, currentUserId?: string | null, ): boolean { if (ACTIONABLE_APPROVAL_STATUSES.has(approval.status)) return true; if (!currentUserId) return false; return approval.requestedByUserId === currentUserId || approval.decidedByUserId === currentUserId; } export function approvalActivityTimestamp(approval: Approval): number { const updatedAt = normalizeTimestamp(approval.updatedAt); if (updatedAt > 0) return updatedAt; return normalizeTimestamp(approval.createdAt); } export function getInboxWorkItems({ issues, approvals, failedRuns = [], joinRequests = [], }: { issues: Issue[]; approvals: Approval[]; failedRuns?: HeartbeatRun[]; joinRequests?: JoinRequest[]; }): InboxWorkItem[] { return [ ...issues.map((issue) => ({ kind: "issue" as const, timestamp: issueLastActivityTimestamp(issue), issue, })), ...approvals.map((approval) => ({ kind: "approval" as const, timestamp: approvalActivityTimestamp(approval), approval, })), ...failedRuns.map((run) => ({ kind: "failed_run" as const, timestamp: normalizeTimestamp(run.createdAt), run, })), ...joinRequests.map((joinRequest) => ({ kind: "join_request" as const, timestamp: normalizeTimestamp(joinRequest.createdAt), joinRequest, })), ].sort((a, b) => { const timestampDiff = b.timestamp - a.timestamp; if (timestampDiff !== 0) return timestampDiff; if (a.kind === "issue" && b.kind === "issue") { return sortIssuesByMostRecentActivity(a.issue, b.issue); } if (a.kind === "approval" && b.kind === "approval") { return approvalActivityTimestamp(b.approval) - approvalActivityTimestamp(a.approval); } return a.kind === "approval" ? -1 : 1; }); } const inboxWorkItemKindOrder: InboxWorkItem["kind"][] = [ "issue", "approval", "failed_run", "join_request", ]; const inboxWorkItemKindLabels: Record = { issue: "Issues", approval: "Approvals", failed_run: "Failed runs", join_request: "Join requests", }; function resolveIssueAssigneeGroup( issue: Pick, { agentById, currentUserId, userLabelById, }: Pick, ): { key: string; label: string } { if (issue.assigneeAgentId) { const agentName = agentById?.get(issue.assigneeAgentId)?.trim(); return { key: `assignee:agent:${issue.assigneeAgentId}`, label: agentName || issue.assigneeAgentId.slice(0, 8), }; } if (issue.assigneeUserId) { return { key: `assignee:user:${issue.assigneeUserId}`, label: formatAssigneeUserLabel(issue.assigneeUserId, currentUserId, userLabelById) ?? "User", }; } return { key: "assignee:none", label: "Unassigned" }; } function resolveIssueProjectGroup( issue: Pick, { projectById }: Pick, ): { key: string; label: string } { if (!issue.projectId) return { key: "project:none", label: "No project" }; const projectName = projectById?.get(issue.projectId)?.name?.trim(); return { key: `project:${issue.projectId}`, label: projectName || issue.projectId.slice(0, 8), }; } function groupInboxWorkItemsByIssueGroup( items: InboxWorkItem[], resolveIssueGroup: (issue: Issue) => { key: string; label: string }, ): InboxWorkItemGroup[] { const groups = new Map(); for (const item of items) { const resolvedGroup = item.kind === "issue" ? resolveIssueGroup(item.issue) : { key: `kind:${item.kind}`, label: inboxWorkItemKindLabels[item.kind] }; const existing = groups.get(resolvedGroup.key); if (existing) { existing.items.push(item); existing.latestTimestamp = Math.max(existing.latestTimestamp, item.timestamp); } else { groups.set(resolvedGroup.key, { label: resolvedGroup.label, items: [item], latestTimestamp: item.timestamp, }); } } return [...groups.entries()] .map(([key, value]) => ({ key, label: value.label, items: value.items, latestTimestamp: value.latestTimestamp, })) .sort((a, b) => { const timestampDiff = b.latestTimestamp - a.latestTimestamp; if (timestampDiff !== 0) return timestampDiff; return a.label.localeCompare(b.label); }) .map(({ key, label, items: groupItems }) => ({ key, label, items: groupItems, })); } export function groupInboxWorkItems( items: InboxWorkItem[], groupBy: InboxWorkItemGroupBy, options: InboxWorkspaceGroupingOptions = {}, ): InboxWorkItemGroup[] { if (groupBy === "none") { return [{ key: "__all", label: null, items }]; } if (groupBy === "workspace") { return groupInboxWorkItemsByIssueGroup(items, (issue) => resolveIssueWorkspaceGroup(issue, options)); } if (groupBy === "assignee") { return groupInboxWorkItemsByIssueGroup(items, (issue) => resolveIssueAssigneeGroup(issue, options)); } if (groupBy === "project") { return groupInboxWorkItemsByIssueGroup(items, (issue) => resolveIssueProjectGroup(issue, options)); } const groups = new Map(); for (const item of items) { const existing = groups.get(item.kind) ?? []; existing.push(item); groups.set(item.kind, existing); } const orderedGroups: InboxWorkItemGroup[] = []; for (const kind of inboxWorkItemKindOrder) { const groupItems = groups.get(kind) ?? []; if (groupItems.length === 0) continue; orderedGroups.push({ key: kind, label: inboxWorkItemKindLabels[kind], items: groupItems, }); } return orderedGroups; } function stripInboxSearchGroupPrefix(groupKey: string) { return groupKey .replace(/^archived-search:/, "") .replace(/^other-search:/, ""); } function firstIssueFromInboxWorkItems(items: InboxWorkItem[]): Issue | null { return items.find((item): item is InboxWorkItem & { kind: "issue" } => item.kind === "issue")?.issue ?? null; } function projectIdForProjectWorkspace( projectWorkspaceId: string | null | undefined, options: InboxWorkspaceGroupingOptions, fallbackIssue: Issue | null, ) { if (!projectWorkspaceId) return fallbackIssue?.projectId ?? null; return options.projectWorkspaceById?.get(projectWorkspaceId)?.projectId ?? (fallbackIssue?.projectWorkspaceId === projectWorkspaceId ? fallbackIssue.projectId : null); } export function buildInboxIssueGroupCreateDefaults( groupKey: string, groupBy: InboxWorkItemGroupBy, items: InboxWorkItem[], options: InboxWorkspaceGroupingOptions = {}, ): InboxIssueGroupCreateDefaults | null { const fallbackIssue = firstIssueFromInboxWorkItems(items); if (!fallbackIssue) return null; const key = stripInboxSearchGroupPrefix(groupKey); if (groupBy === "project") { if (!key.startsWith("project:")) return {}; const projectId = key.slice("project:".length); return projectId && projectId !== "none" ? { projectId } : {}; } if (groupBy === "assignee") { if (key.startsWith("assignee:agent:")) { const assigneeAgentId = key.slice("assignee:agent:".length); return assigneeAgentId ? { assigneeAgentId } : {}; } if (key.startsWith("assignee:user:")) { const assigneeUserId = key.slice("assignee:user:".length); return assigneeUserId ? { assigneeUserId } : {}; } return {}; } if (groupBy === "workspace") { if (key.startsWith("workspace:execution:")) { const executionWorkspaceId = key.slice("workspace:execution:".length); if (!executionWorkspaceId) return {}; const executionWorkspace = options.executionWorkspaceById?.get(executionWorkspaceId) ?? null; const projectWorkspaceId = executionWorkspace?.projectWorkspaceId ?? (fallbackIssue.executionWorkspaceId === executionWorkspaceId ? fallbackIssue.projectWorkspaceId : null); const projectId = executionWorkspace?.projectId ?? projectIdForProjectWorkspace(projectWorkspaceId, options, fallbackIssue); return { executionWorkspaceId, executionWorkspaceMode: "reuse_existing", ...(projectId ? { projectId } : {}), ...(projectWorkspaceId ? { projectWorkspaceId } : {}), }; } if (key.startsWith("workspace:project:")) { const projectWorkspaceId = key.slice("workspace:project:".length); if (!projectWorkspaceId) return {}; const projectId = projectIdForProjectWorkspace(projectWorkspaceId, options, fallbackIssue); return { ...(projectId ? { projectId } : {}), projectWorkspaceId, }; } } return {}; } /** * Groups parent-child issues in a flat InboxWorkItem list. * * - Children whose parent is also in the list are removed from the top level * and stored in `childrenByIssueId`. * - The parent's sort timestamp becomes max(parent, children) so that a group * with a recently-updated child floats to the top. * - If a parent is absent (e.g. archived), children remain as independent roots. */ export function buildInboxNesting(items: InboxWorkItem[]): { displayItems: InboxWorkItem[]; childrenByIssueId: Map; } { const issueItems: (InboxWorkItem & { kind: "issue" })[] = []; const nonIssueItems: InboxWorkItem[] = []; for (const item of items) { if (item.kind === "issue") issueItems.push(item as InboxWorkItem & { kind: "issue" }); else nonIssueItems.push(item); } const issueIdSet = new Set(issueItems.map((i) => i.issue.id)); const childrenByIssueId = new Map(); const childIds = new Set(); for (const item of issueItems) { const { issue } = item; if (issue.parentId && issueIdSet.has(issue.parentId)) { childIds.add(issue.id); const arr = childrenByIssueId.get(issue.parentId) ?? []; arr.push(issue); childrenByIssueId.set(issue.parentId, arr); } } const subtreeActivityTimestamp = (issue: Issue, seen: ReadonlySet = new Set()): number => { const ownTimestamp = issueLastActivityTimestamp(issue); if (seen.has(issue.id)) return ownTimestamp; const nextSeen = new Set(seen); nextSeen.add(issue.id); const children = childrenByIssueId.get(issue.id) ?? []; if (children.length === 0) return ownTimestamp; return Math.max( ownTimestamp, ...children.map((child) => subtreeActivityTimestamp(child, nextSeen)), ); }; // Sort each child list by most recent descendant activity, not just direct issue activity. for (const children of childrenByIssueId.values()) { children.sort((a, b) => { const activityDiff = subtreeActivityTimestamp(b) - subtreeActivityTimestamp(a); if (activityDiff !== 0) return activityDiff; return sortIssuesByMostRecentActivity(a, b); }); } // Build root issue items with group-adjusted timestamps const rootIssueItems: InboxWorkItem[] = issueItems .filter((item) => !childIds.has(item.issue.id)) .map((item) => { const children = childrenByIssueId.get(item.issue.id); if (!children?.length) return item; const maxChildTs = Math.max(...children.map((child) => subtreeActivityTimestamp(child))); return { ...item, timestamp: Math.max(item.timestamp, maxChildTs) }; }); // Merge and re-sort const displayItems = [...rootIssueItems, ...nonIssueItems].sort((a, b) => { const diff = b.timestamp - a.timestamp; if (diff !== 0) return diff; if (a.kind === "issue" && b.kind === "issue") { return sortIssuesByMostRecentActivity(a.issue, b.issue); } return 0; }); return { displayItems, childrenByIssueId }; } export function buildGroupedInboxSections( items: InboxWorkItem[], groupBy: InboxWorkItemGroupBy, workspaceGrouping: InboxWorkspaceGroupingOptions, options?: { keyPrefix?: string; searchSection?: InboxSearchSection; nestingEnabled?: boolean }, ): InboxGroupedSection[] { const keyPrefix = options?.keyPrefix ?? ""; const searchSection = options?.searchSection ?? "none"; const nestingEnabled = options?.nestingEnabled ?? false; return groupInboxWorkItems(items, groupBy, workspaceGrouping).map((group) => { const nestedGroup = nestingEnabled && group.items.some((item) => item.kind === "issue") ? buildInboxNesting(group.items) : { displayItems: group.items, childrenByIssueId: new Map() }; return { key: `${keyPrefix}${group.key}`, label: group.label, displayItems: nestedGroup.displayItems, childrenByIssueId: nestedGroup.childrenByIssueId, searchSection, }; }); } export function getInboxWorkItemKey(item: InboxWorkItem): string { if (item.kind === "issue") return `issue:${item.issue.id}`; if (item.kind === "approval") return `approval:${item.approval.id}`; if (item.kind === "failed_run") return `run:${item.run.id}`; return `join:${item.joinRequest.id}`; } export function buildInboxKeyboardNavEntries( groupedSections: ReadonlyArray, collapsedGroupKeys: ReadonlySet, collapsedInboxParents: ReadonlySet, ): InboxKeyboardNavEntry[] { const entries: InboxKeyboardNavEntry[] = []; for (const group of groupedSections) { const isCollapsed = collapsedGroupKeys.has(group.key); if (group.label) { entries.push({ type: "group", groupKey: group.key, label: group.label, collapsed: isCollapsed, }); } if (isCollapsed) continue; const addIssueChildren = (issueId: string, seen: ReadonlySet) => { const children = group.childrenByIssueId.get(issueId); if (!children?.length || collapsedInboxParents.has(issueId)) return; for (const child of children) { if (seen.has(child.id)) continue; const nextSeen = new Set(seen); nextSeen.add(child.id); entries.push({ type: "child", issueId: child.id, issue: child, }); addIssueChildren(child.id, nextSeen); } }; for (const item of group.displayItems) { entries.push({ type: "top", itemKey: `${group.key}:${getInboxWorkItemKey(item)}`, item, }); if (item.kind !== "issue") continue; addIssueChildren(item.issue.id, new Set([item.issue.id])); } } return entries; } export function shouldShowInboxSection({ tab, hasItems, showOnMine, showOnRecent, showOnUnread, showOnAll, }: { tab: InboxTab; hasItems: boolean; showOnMine: boolean; showOnRecent: boolean; showOnUnread: boolean; showOnAll: boolean; }): boolean { if (!hasItems) return false; if (tab === "mine") return showOnMine; if (tab === "recent") return showOnRecent; if (tab === "unread") return showOnUnread; return showOnAll; } export function computeInboxBadgeData({ approvals, joinRequests, dashboard, heartbeatRuns, mineIssues, dismissedAlerts, dismissedAtByKey, currentUserId, }: { approvals: Approval[]; joinRequests: JoinRequest[]; dashboard: DashboardSummary | undefined; heartbeatRuns: HeartbeatRun[]; mineIssues: Issue[]; dismissedAlerts: Set; dismissedAtByKey: ReadonlyMap; currentUserId?: string | null; }): InboxBadgeData { const actionableApprovals = approvals.filter( (approval) => isApprovalVisibleInMine(approval, currentUserId) && ACTIONABLE_APPROVAL_STATUSES.has(approval.status) && !isInboxEntityDismissed(dismissedAtByKey, `approval:${approval.id}`, approval.updatedAt), ).length; const failedRuns = getLatestFailedRunsByAgent(heartbeatRuns).filter( (run) => !isInboxEntityDismissed(dismissedAtByKey, `run:${run.id}`, run.createdAt), ).length; const visibleJoinRequests = joinRequests.filter( (jr) => !isInboxEntityDismissed(dismissedAtByKey, `join:${jr.id}`, jr.updatedAt ?? jr.createdAt), ).length; const visibleMineIssues = mineIssues.filter((issue) => issue.isUnreadForMe).length; const agentErrorCount = dashboard?.agents.error ?? 0; const monthBudgetCents = dashboard?.costs.monthBudgetCents ?? 0; const monthUtilizationPercent = dashboard?.costs.monthUtilizationPercent ?? 0; const showAggregateAgentError = agentErrorCount > 0 && failedRuns === 0 && !dismissedAlerts.has("alert:agent-errors"); const showBudgetAlert = monthBudgetCents > 0 && monthUtilizationPercent >= 80 && !dismissedAlerts.has("alert:budget"); const alerts = Number(showAggregateAgentError) + Number(showBudgetAlert); return { // The inbox badge reflects personal/actionable work, not company-wide health alerts. inbox: actionableApprovals + visibleJoinRequests + failedRuns + visibleMineIssues, approvals: actionableApprovals, failedRuns, joinRequests: visibleJoinRequests, mineIssues: visibleMineIssues, alerts, }; }