TaskStatusPill

Summarize background tasks in a status bar or header, for example 2 running · 1 failed. Pass flattenTaskTree(tasks) to count subtasks too, or a precomputed summary. The pill is blue while tasks run or wait and neutral otherwise; only the failed count is colored as an error. With onOpen the pill is a button, typically opening BackgroundTasksDrawer. It renders nothing without tasks unless showWhenEmpty is set.

Getting Started
$pnpm add @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-react
Examples
States
Source

The component source as it ships in the package. Useful as a reference, or as a starting point if you want to fork a piece.

tasks/TaskStatusPill/TaskStatusPill.tsx
import React, { Fragment, memo, useMemo } from 'react';
import { Badge, Button, Loader, type MantineColor } from '@mantine/core';
import {
  describeTaskSummary,
  DEFAULT_BACKGROUND_TASK_LABELS,
  flattenTaskTree,
  summarizeTasks,
} from '../task-utils';
import type { BackgroundTask, BackgroundTaskLabels, BackgroundTaskSummary } from '../types';
import classes from './TaskStatusPill.module.css';

export interface TaskStatusPillProps {
  /** Tasks to summarize, nested `children` included */
  tasks?: BackgroundTask[];
  /** Counts subtasks from `children` as well as top-level tasks, `true` by default */
  countSubtasks?: boolean;
  /** Precomputed summary, takes precedence over `tasks` */
  summary?: BackgroundTaskSummary;
  /** Called on click, renders the pill as a button when set */
  onOpen?: () => void;
  /** Renders `pillEmpty` instead of nothing when there are no tasks, `false` by default */
  showWhenEmpty?: boolean;
  /** Overrides of the default English labels */
  labels?: Partial<BackgroundTaskLabels>;
  /** Class name added to the root element */
  className?: string;
  /** Inline styles added to the root element */
  style?: React.CSSProperties;
}

function pickColor(summary: BackgroundTaskSummary): MantineColor {
  return summary.running > 0 || summary.queued > 0 ? 'blue' : 'gray';
}

/** Compact background task indicator for a status bar or header, for example `2 running · 1 failed` */
export const TaskStatusPill = memo(function TaskStatusPill({
  tasks,
  summary: summaryProp,
  countSubtasks = true,
  onOpen,
  showWhenEmpty = false,
  labels: labelsProp,
  className,
  style,
}: TaskStatusPillProps) {
  const labels = { ...DEFAULT_BACKGROUND_TASK_LABELS, ...labelsProp };
  const summary = useMemo(
    () =>
      summaryProp ?? summarizeTasks(countSubtasks ? flattenTaskTree(tasks ?? []) : (tasks ?? [])),
    [summaryProp, tasks, countSubtasks]
  );
  const parts = describeTaskSummary(summary, labels);

  if (parts.length === 0 && !showWhenEmpty) {
    return null;
  }

  const failedIndex = summary.failed > 0 ? parts.length - 1 : -1;
  const text =
    parts.length > 0 ? (
      <span>
        {parts.map((part, index) => (
          <Fragment key={part}>
            {index > 0 && ' · '}
            {index === failedIndex ? <span className={classes.failed}>{part}</span> : part}
          </Fragment>
        ))}
      </span>
    ) : (
      labels.pillEmpty
    );
  const color = pickColor(summary);
  const busy = summary.running > 0;

  if (onOpen) {
    return (
      <Button
        variant="light"
        color={color}
        size="compact-xs"
        radius="xl"
        onClick={onOpen}
        leftSection={busy ? <Loader size={10} color={color} aria-hidden /> : undefined}
        className={className}
        style={style}
        data-busy={busy || undefined}
      >
        {text}
      </Button>
    );
  }

  return (
    <Badge
      variant="light"
      color={color}
      size="sm"
      tt="none"
      leftSection={busy ? <Loader size={10} color={color} aria-hidden /> : undefined}
      className={className}
      style={style}
      role="status"
      data-busy={busy || undefined}
    >
      {text}
    </Badge>
  );
});

TaskStatusPill.displayName = 'TaskStatusPill';
API reference
Prop
Type
Required
tasks
BackgroundTask[]
No
Tasks to summarize, nested `children` included
countSubtasks
boolean
No
Counts subtasks from `children` as well as top-level tasks, `true` by default
summary
BackgroundTaskSummary
No
Precomputed summary, takes precedence over `tasks`
onOpen
() => void
No
Called on click, renders the pill as a button when set
showWhenEmpty
boolean
No
Renders `pillEmpty` instead of nothing when there are no tasks, `false` by default
labels
Partial<BackgroundTaskLabels>
No
Overrides of the default English labels
className
string
No
Class name added to the root element
style
React.CSSProperties
No
Inline styles added to the root element