BackgroundTasksPanel
Monitor background work of an agent session: shell commands, subagents, remote jobs and workflows. TaskList and TaskDetail sit side by side in MasterDetail from 720px of width; narrower, the detail replaces the list with a back action. The selected task can be a subtask, controlled with selectedId or uncontrolled with defaultSelectedId. BackgroundTasksDrawer opens the same panel in a drawer that slides from the bottom on small screens, usually from a TaskStatusPill.
Getting Started
$pnpm add @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-reactExamples
Wide
Narrow
Drawer from a status pill
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/BackgroundTasksPanel/BackgroundTasksPanel.tsx
import React, { memo, useMemo, useState } from 'react';
import { Box, EmptyState, Group, Stack } from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import { IconListDetails } from '@tabler/icons-react';
import { MasterDetail } from '../../primitives/MasterDetail/MasterDetail';
import { TaskDetail } from '../TaskDetail/TaskDetail';
import { TaskList, type TaskAction } from '../TaskList/TaskList';
import { DEFAULT_BACKGROUND_TASK_LABELS, findParentTask, findTask } from '../task-utils';
import type { BackgroundTask, BackgroundTaskLabels } from '../types';
import { cx } from '../../utils/cx';
import classes from './BackgroundTasksPanel.module.css';
export interface BackgroundTasksPanelProps {
/** Top-level tasks with nested `children` */
tasks: BackgroundTask[];
/** Id of the task shown in the detail, controlled; may point at a subtask */
selectedId?: string | null;
/** Initial selected task id when uncontrolled */
defaultSelectedId?: string | null;
/** Called when the selected task changes */
onSelectedIdChange?: (id: string | null) => void;
/** Stops a queued or running task */
onStop?: TaskAction;
/** Retries a failed or cancelled task */
onRetryTask?: TaskAction;
/** Removes a finished task */
onRemove?: TaskAction;
/** Shows skeleton rows in the list */
loading?: boolean;
/** Error message shown instead of the list */
error?: React.ReactNode;
/** Called by the retry button of the error alert */
onRetry?: () => void;
/** Sends an instruction to a running agent task from its detail */
onSteer?: (taskId: string, text: string) => void | Promise<void>;
/** Shows at most this many tasks in the list with a summary of the rest */
maxVisible?: number;
/** Marks the task search input with `data-autofocus` so an enclosing Drawer or Modal focuses it on open */
searchAutofocus?: boolean;
/** Content at the start of the first row, on the same inner edge as the list, for example a panel title */
header?: React.ReactNode;
/** Ghost list toolbar with the search behind an icon; follows the width when omitted */
compact?: boolean;
/** List pane width in px when wide, `340` by default */
listWidth?: number;
/** Component width in px from which list and detail sit side by side, `720` by default */
breakpoint?: number;
/** 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;
}
const DEFAULT_BREAKPOINT = 720;
/** Background tasks as list and detail: side by side when wide, stacked with a back action when narrow */
export const BackgroundTasksPanel = memo(function BackgroundTasksPanel({
tasks,
selectedId: selectedIdProp,
defaultSelectedId = null,
onSelectedIdChange,
onStop,
onRetryTask,
onRemove,
loading,
error,
onRetry,
searchAutofocus,
onSteer,
maxVisible,
header,
compact: compactProp,
listWidth = 340,
breakpoint,
labels: labelsProp,
className,
style,
}: BackgroundTasksPanelProps) {
const { ref, width } = useElementSize<HTMLDivElement>();
const measuring = compactProp === undefined && width === 0;
const compact = compactProp ?? width < (breakpoint ?? DEFAULT_BREAKPOINT);
const labels = useMemo(
() => ({ ...DEFAULT_BACKGROUND_TASK_LABELS, ...labelsProp }),
[labelsProp]
);
const [innerSelectedId, setInnerSelectedId] = useState(defaultSelectedId);
const selectedId = selectedIdProp !== undefined ? selectedIdProp : innerSelectedId;
const selected = findTask(tasks, selectedId);
const setSelectedId = (id: string | null) => {
setInnerSelectedId(id);
onSelectedIdChange?.(id);
};
const rootId = tasks.find((task) => findTask([task], selectedId))?.id ?? null;
const handleBack = () => {
setSelectedId(findParentTask(tasks, selectedId)?.id ?? null);
};
return (
<Stack ref={ref} gap={0} className={cx(classes.root, className)} style={style}>
{header !== undefined && (
<Group gap="xs" wrap="nowrap" className={cx(classes.inset, classes.panelHeader)}>
{header}
</Group>
)}
<Box className={classes.body}>
<MasterDetail
listWidth={listWidth}
breakpoint={breakpoint}
labels={{ back: labels.back }}
onBack={handleBack}
emptyDetail={
<EmptyState
h="100%"
p="xl"
icon={<IconListDetails />}
title={labels.selectTask}
description={labels.selectTaskDescription}
/>
}
list={
<Box
py="sm"
className={cx(classes.inset, classes.measured)}
data-measuring={measuring || undefined}
>
<TaskList
tasks={tasks}
selectedId={rootId}
onSelect={(task) => setSelectedId(task.id)}
onStop={onStop}
onRetryTask={onRetryTask}
onRemove={onRemove}
loading={loading}
error={error}
onRetry={onRetry}
searchAutofocus={searchAutofocus}
compact={compact}
maxVisible={maxVisible}
labels={labelsProp}
/>
</Box>
}
detail={
selected ? (
<TaskDetail
key={selected.id}
task={selected}
onStop={onStop}
onRetryTask={onRetryTask}
onSteer={onSteer}
allTasks={tasks}
onSelectSubtask={(task) => setSelectedId(task.id)}
labels={labelsProp}
/>
) : null
}
/>
</Box>
</Stack>
);
});
BackgroundTasksPanel.displayName = 'BackgroundTasksPanel';
API reference
Prop
Type
Required
tasks
BackgroundTask[]
Yes
Top-level tasks with nested `children`
selectedId
string | null
No
Id of the task shown in the detail, controlled; may point at a subtask
defaultSelectedId
string | null
No
Initial selected task id when uncontrolled
onSelectedIdChange
(id: string | null) => void
No
Called when the selected task changes
onStop
TaskAction
No
Stops a queued or running task
onRetryTask
TaskAction
No
Retries a failed or cancelled task
onRemove
TaskAction
No
Removes a finished task
loading
boolean
No
Shows skeleton rows in the list
error
React.ReactNode
No
Error message shown instead of the list
onRetry
() => void
No
Called by the retry button of the error alert
onSteer
(taskId: string, text: string) => void | Promise<void>
No
Sends an instruction to a running agent task from its detail
maxVisible
number
No
Shows at most this many tasks in the list with a summary of the rest
searchAutofocus
boolean
No
Marks the task search input with `data-autofocus` so an enclosing Drawer or Modal focuses it on open
header
React.ReactNode
No
Content at the start of the first row, on the same inner edge as the list, for example a panel title
compact
boolean
No
Ghost list toolbar with the search behind an icon; follows the width when omitted
listWidth
number
No
List pane width in px when wide, `340` by default
breakpoint
number
No
Component width in px from which list and detail sit side by side, `720` 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