AgentList
List subagent definitions with avatar, description and model, grouped by source (built-in, user, project, plugin) unless groupBySource is false. The list is searchable and filters by source when agents come from more than one (withSearch and withSourceFilter turn these off), offers a New agent button when onCreate is set and an actions menu with Edit, Duplicate and Delete; Edit and Delete are disabled for read-only agents. Handles loading, error and empty states.
Getting Started
$pnpm add @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-reactExamples
Agents
Loading, error and empty
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.
agents/AgentList/AgentList.tsx
import React, { memo, useCallback, useState } from 'react';
import { Badge, Button, Group, Text } from '@mantine/core';
import {
IconCopy,
IconLock,
IconPencil,
IconPlus,
IconRobot,
IconTrash,
} from '@tabler/icons-react';
import { EntityList, type EntityListItemState } from '../../primitives/EntityList/EntityList';
import {
EntityListItem,
type EntityListItemAction,
} from '../../primitives/EntityList/EntityListItem';
import type { ModelOption } from '../../types';
import { getAgentModelLabel } from '../agent-display';
import { AgentAvatar } from '../AgentAvatar/AgentAvatar';
import type { AgentDefinition, AgentSource } from '../types';
import { AGENT_SOURCE_ORDER, matchesAgentQuery } from '../validate-agent';
export interface AgentListLabels {
list: string;
search: string;
source: string;
allSources: string;
newAgent: string;
edit: string;
duplicate: string;
delete: string;
actions: string;
readOnly: string;
inheritModel: string;
noResults: string;
emptyTitle: string;
emptyDescription: string;
retry: string;
sources: Record<AgentSource, string>;
}
export const DEFAULT_AGENT_LIST_LABELS: AgentListLabels = {
list: 'Agents',
search: 'Search agents',
source: 'Source',
allSources: 'All sources',
newAgent: 'New agent',
edit: 'Edit',
duplicate: 'Duplicate',
delete: 'Delete',
actions: 'Agent actions',
readOnly: 'Read-only',
inheritModel: 'Inherit',
noResults: 'No agents match the search',
emptyTitle: 'No agents yet',
emptyDescription: 'Create an agent to delegate focused tasks with its own prompt and tools',
retry: 'Retry',
sources: { builtin: 'Built-in', user: 'User', project: 'Project', plugin: 'Plugin' },
};
export interface AgentListProps {
/** Agents to list */
agents: AgentDefinition[];
/** Id of the selected agent */
selectedId?: string | null;
/** Called when an agent row is chosen */
onSelect?: (agent: AgentDefinition) => void;
/** Called by the New agent button, the button is rendered only when set */
onCreate?: () => void;
/** Called by the Edit action, disabled for read-only agents */
onEdit?: (agent: AgentDefinition) => void;
/** Called by the Duplicate action */
onDuplicate?: (agent: AgentDefinition) => void;
/** Called by the Delete action, disabled for read-only agents */
onDelete?: (agent: AgentDefinition) => void;
/** Models used to show a readable model name */
models?: ModelOption[];
/** Groups agents under source headers, `true` by default */
groupBySource?: boolean;
/** Shows the search input, `true` by default */
withSearch?: boolean;
/** Shows the source filter when agents come from more than one source, `true` by default */
withSourceFilter?: boolean;
/** Shows skeleton rows */
loading?: boolean;
/** Error message shown instead of the list */
error?: React.ReactNode;
/** Called by the retry button of the error alert */
onRetry?: () => void;
/** Overrides of the default English labels */
labels?: Partial<AgentListLabels>;
/** Class name added to the root element */
className?: string;
/** Inline styles added to the root element */
style?: React.CSSProperties;
}
/** Searchable list of agents grouped by source, with edit, duplicate and delete actions */
export const AgentList = memo(function AgentList({
agents,
selectedId,
onSelect,
onCreate,
onEdit,
onDuplicate,
onDelete,
models = [],
groupBySource = true,
withSearch = true,
withSourceFilter = true,
loading,
error,
onRetry,
labels: labelsProp,
className,
style,
}: AgentListProps) {
const labels = {
...DEFAULT_AGENT_LIST_LABELS,
...labelsProp,
sources: { ...DEFAULT_AGENT_LIST_LABELS.sources, ...labelsProp?.sources },
};
const [query, setQuery] = useState('');
const [source, setSource] = useState<string>('all');
const getActions = (agent: AgentDefinition): EntityListItemAction[] => {
const actions: EntityListItemAction[] = [];
if (onEdit) {
actions.push({
label: labels.edit,
icon: <IconPencil size={14} />,
disabled: agent.readOnly,
onClick: () => onEdit(agent),
});
}
if (onDuplicate) {
actions.push({
label: labels.duplicate,
icon: <IconCopy size={14} />,
onClick: () => onDuplicate(agent),
});
}
if (onDelete) {
actions.push({
label: labels.delete,
icon: <IconTrash size={14} />,
color: 'red',
disabled: agent.readOnly,
onClick: () => onDelete(agent),
});
}
return actions;
};
const getId = useCallback((agent: AgentDefinition) => agent.id, []);
const sourceLabels = labels.sources;
const groupBy = useCallback(
(agent: AgentDefinition) => sourceLabels[agent.source],
[sourceLabels]
);
const sourceOptions = AGENT_SOURCE_ORDER.map((value) => ({
value,
label: sourceLabels[value],
count: agents.filter((agent) => agent.source === value).length,
})).filter((option) => option.count > 0);
const showSourceFilter = withSourceFilter && sourceOptions.length > 1;
const activeSource = sourceOptions.some((option) => option.value === source) ? source : 'all';
return (
<EntityList<AgentDefinition>
items={agents}
getId={getId}
selectedId={selectedId}
onSelect={onSelect}
loading={loading}
error={error}
onRetry={onRetry}
labels={{ retry: labels.retry, noResults: labels.noResults }}
ariaLabel={labels.list}
groupBy={groupBySource ? groupBy : undefined}
groupOrder={AGENT_SOURCE_ORDER.map((source) => labels.sources[source])}
search={
withSearch
? {
value: query,
onChange: setQuery,
placeholder: labels.search,
filter: matchesAgentQuery,
}
: undefined
}
filters={
showSourceFilter
? {
value: activeSource,
onChange: setSource,
label: labels.source,
filter: (agent, value) => value === 'all' || agent.source === value,
options: [
{ value: 'all', label: labels.allSources, count: agents.length },
...sourceOptions,
],
}
: undefined
}
empty={{
icon: <IconRobot />,
title: labels.emptyTitle,
description: labels.emptyDescription,
action: onCreate && (
<Button size="xs" leftSection={<IconPlus size={14} />} onClick={onCreate}>
{labels.newAgent}
</Button>
),
}}
toolbar={
onCreate && (
<Button size="sm" leftSection={<IconPlus size={16} />} onClick={onCreate}>
{labels.newAgent}
</Button>
)
}
className={className}
style={style}
renderItem={(agent: AgentDefinition, { selected }: EntityListItemState) => (
<EntityListItem
selected={selected}
icon={<AgentAvatar agent={agent} size="md" />}
title={agent.displayName || agent.name}
description={agent.description}
actions={getActions(agent)}
labels={{ actions: `${labels.actions}: ${agent.displayName || agent.name}` }}
badges={
<Group gap={4} wrap="nowrap">
{!groupBySource && (
<Badge size="xs" variant="light" color="gray">
{labels.sources[agent.source]}
</Badge>
)}
{agent.readOnly && <IconLock size={12} aria-label={labels.readOnly} role="img" />}
</Group>
}
meta={
<Text component="span" size="xs" c="dimmed">
{getAgentModelLabel(agent.model, models, labels.inheritModel)}
</Text>
}
/>
)}
/>
);
});
AgentList.displayName = 'AgentList';
Depends on:
API reference
Prop
Type
Required
agents
AgentDefinition[]
Yes
Agents to list
selectedId
string | null
No
Id of the selected agent
onSelect
(agent: AgentDefinition) => void
No
Called when an agent row is chosen
onCreate
() => void
No
Called by the New agent button, the button is rendered only when set
onEdit
(agent: AgentDefinition) => void
No
Called by the Edit action, disabled for read-only agents
onDuplicate
(agent: AgentDefinition) => void
No
Called by the Duplicate action
onDelete
(agent: AgentDefinition) => void
No
Called by the Delete action, disabled for read-only agents
models
ModelOption[]
No
Models used to show a readable model name
groupBySource
boolean
No
Groups agents under source headers, `true` by default
withSearch
boolean
No
Shows the search input, `true` by default
withSourceFilter
boolean
No
Shows the source filter when agents come from more than one source, `true` by default
loading
boolean
No
Shows skeleton rows
error
React.ReactNode
No
Error message shown instead of the list
onRetry
() => void
No
Called by the retry button of the error alert
labels
Partial<AgentListLabels>
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