StatusPanel

Summarize the agent environment, like a /status command: version, model, account and organization, working directory, MCP servers counted by status, loaded memory files and context window usage with ContextUsage. Add rows with items and buttons with actions; an action that returns a promise shows a loader until it settles.

Getting Started
$pnpm add @sinups/ai-kit @mantine/core @mantine/hooks @tabler/icons-react
Examples
Wide
Narrow
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.

model-settings/StatusPanel/StatusPanel.tsx
import React, { memo } from 'react';
import { Alert, Badge, Button, Code, DataList, Group, Stack, Text, Title } from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import { IconAlertCircle } from '@tabler/icons-react';
import { usePendingActions } from '../../hooks/use-pending-actions';
import { ContextUsage, type ContextUsageSegment } from '../../ContextUsage/ContextUsage';
import type { AgentUiStatus } from '../../primitives/StatusBadge/status-meta';
import { StatusBadge } from '../../primitives/StatusBadge/StatusBadge';
import { formatTokens } from '../../utils/format-tokens';
import { countMcpStatuses } from '../status-panel';
import type { StatusAction, StatusMcpServer, StatusMemoryFile } from '../types';
import classes from './StatusPanel.module.css';

export interface StatusPanelLabels {
  title: string;
  version: string;
  model: string;
  account: string;
  organization: string;
  cwd: string;
  mcpServers: string;
  memory: string;
  context: string;
  none: string;
  mcpStatus: Record<AgentUiStatus, string>;
  tokens: (value: string) => string;
  contextValue: (used: string, total: string) => string;
  actionError: string;
}

export const DEFAULT_STATUS_PANEL_LABELS: StatusPanelLabels = {
  title: 'Status',
  version: 'Version',
  model: 'Model',
  account: 'Account',
  organization: 'Organization',
  cwd: 'Working directory',
  mcpServers: 'MCP servers',
  memory: 'Memory',
  context: 'Context',
  none: 'None',
  mcpStatus: {
    success: 'connected',
    running: 'connecting',
    pending: 'pending',
    'needs-auth': 'need auth',
    warning: 'warning',
    error: 'failed',
    disabled: 'disabled',
    idle: 'idle',
  },
  tokens: (value) => `${value} tokens`,
  contextValue: (used, total) => `${used} / ${total} tokens`,
  actionError: 'Action failed',
};

export interface StatusPanelProps {
  /** Agent or app version */
  version?: string;
  /** Name of the active model */
  model?: React.ReactNode;
  /** Signed-in account */
  account?: { email?: string; name?: string; plan?: string };
  /** Organization of the account */
  organization?: string;
  /** Working directory of the session */
  cwd?: string;
  /** MCP servers, summarized as counters per status */
  mcpServers?: StatusMcpServer[];
  /** Memory files loaded into the context */
  memoryFiles?: StatusMemoryFile[];
  /** Context window usage */
  context?: {
    used: number;
    total: number;
    segments?: ContextUsageSegment[];
    onCompact?: () => void;
  };
  /** Extra rows appended after the built-in ones */
  items?: { label: React.ReactNode; value: React.ReactNode }[];
  /** Buttons under the summary, for example `Doctor` or `Log out` */
  actions?: StatusAction[];
  /** Shows the heading, `true` by default */
  withTitle?: boolean;
  /** Overrides of the default English labels */
  labels?: Partial<StatusPanelLabels>;
  /** Class name added to the root element */
  className?: string;
  /** Inline styles added to the root element */
  style?: React.CSSProperties;
}

/** Environment summary: version, model, account, working directory, MCP servers, memory and context usage */
export const StatusPanel = memo(function StatusPanel({
  version,
  model,
  account,
  organization,
  cwd,
  mcpServers,
  memoryFiles,
  context,
  items = [],
  actions = [],
  withTitle = true,
  labels: labelsProp,
  className,
  style,
}: StatusPanelProps) {
  const labels = {
    ...DEFAULT_STATUS_PANEL_LABELS,
    ...labelsProp,
    mcpStatus: { ...DEFAULT_STATUS_PANEL_LABELS.mcpStatus, ...labelsProp?.mcpStatus },
  };
  const { ref, width } = useElementSize();
  const pending = usePendingActions(labels.actionError);

  const rows: { key: string; label: React.ReactNode; value: React.ReactNode }[] = [];
  if (version) {
    rows.push({ key: 'version', label: labels.version, value: <Code>{version}</Code> });
  }
  if (model) {
    rows.push({ key: 'model', label: labels.model, value: model });
  }
  if (account) {
    rows.push({
      key: 'account',
      label: labels.account,
      value: (
        <Group gap="xs" wrap="wrap">
          <Text inherit truncate="end" miw={0}>
            {account.email ?? account.name}
          </Text>
          {account.plan && (
            <Badge size="xs" variant="light">
              {account.plan}
            </Badge>
          )}
        </Group>
      ),
    });
  }
  if (organization) {
    rows.push({ key: 'organization', label: labels.organization, value: organization });
  }
  if (cwd) {
    rows.push({
      key: 'cwd',
      label: labels.cwd,
      value: (
        <Code title={cwd} className={classes.path}>
          {cwd}
        </Code>
      ),
    });
  }
  if (mcpServers) {
    const counts = countMcpStatuses(mcpServers);
    rows.push({
      key: 'mcp',
      label: labels.mcpServers,
      value:
        counts.length === 0 ? (
          <Text size="sm" c="dimmed">
            {labels.none}
          </Text>
        ) : (
          <Group gap={6} wrap="wrap">
            {counts.map(({ status, count }) => (
              <StatusBadge
                key={status}
                status={status}
                withIcon={status !== 'running' && status !== 'pending'}
                label={`${count} ${labels.mcpStatus[status]}`}
              />
            ))}
          </Group>
        ),
    });
  }
  if (memoryFiles) {
    rows.push({
      key: 'memory',
      label: labels.memory,
      value:
        memoryFiles.length === 0 ? (
          <Text size="sm" c="dimmed">
            {labels.none}
          </Text>
        ) : (
          <Stack gap={4}>
            {memoryFiles.map((file) => (
              <Group key={file.path} gap="xs" wrap="wrap">
                <Code className={classes.path}>{file.path}</Code>
                {file.tokens !== undefined && (
                  <Text size="xs" c="dimmed">
                    {labels.tokens(formatTokens(file.tokens))}
                  </Text>
                )}
              </Group>
            ))}
          </Stack>
        ),
    });
  }
  if (context) {
    rows.push({
      key: 'context',
      label: labels.context,
      value: (
        <Group gap="xs" wrap="nowrap">
          <ContextUsage
            used={context.used}
            total={context.total}
            segments={context.segments}
            onCompact={context.onCompact}
            withLabel
          />
          <Text size="xs" c="dimmed">
            {labels.contextValue(formatTokens(context.used), formatTokens(context.total))}
          </Text>
        </Group>
      ),
    });
  }
  items.forEach((item, index) => rows.push({ key: `item-${index}`, ...item }));

  const anyPending = actions.some((action) => pending.isPending(action.label));

  return (
    <Stack ref={ref} gap="md" className={className} style={style}>
      {withTitle && (
        <Title order={3} size="h5">
          {labels.title}
        </Title>
      )}
      <DataList
        orientation={width > 0 && width < 400 ? 'vertical' : 'horizontal'}
        labelWidth="160px"
      >
        {rows.map((row) => (
          <DataList.Item key={row.key}>
            <DataList.ItemLabel>{row.label}</DataList.ItemLabel>
            <DataList.ItemValue>{row.value}</DataList.ItemValue>
          </DataList.Item>
        ))}
      </DataList>
      {pending.error && (
        <Alert color="red" variant="light" icon={<IconAlertCircle size={16} />}>
          {pending.error}
        </Alert>
      )}
      {actions.length > 0 && (
        <Group gap="xs">
          {actions.map((action) => (
            <Button
              key={action.label}
              size="xs"
              variant="default"
              leftSection={action.icon}
              loading={pending.isPending(action.label)}
              disabled={anyPending && !pending.isPending(action.label)}
              onClick={() => pending.run(action.label, action.onClick)}
            >
              {action.label}
            </Button>
          ))}
        </Group>
      )}
    </Stack>
  );
});

StatusPanel.displayName = 'StatusPanel';
API reference
Prop
Type
Required
version
string
No
Agent or app version
model
React.ReactNode
No
Name of the active model
account
{ email?: string; name?: string; plan?: string }
No
Signed-in account
organization
string
No
Organization of the account
cwd
string
No
Working directory of the session
mcpServers
StatusMcpServer[]
No
MCP servers, summarized as counters per status
memoryFiles
StatusMemoryFile[]
No
Memory files loaded into the context
context
{ used: number; total: number; segments?: ContextUsageSegment[]; onCompact?: () => void; }
No
Context window usage
items
{ label: React.ReactNode; value: React.ReactNode }[]
No
Extra rows appended after the built-in ones
actions
StatusAction[]
No
Buttons under the summary, for example `Doctor` or `Log out`
withTitle
boolean
No
Shows the heading, `true` by default
labels
Partial<StatusPanelLabels>
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