ModeSelector

Standalone mode selector: agent mode, plan mode, or any custom set. Bring your own icons or omit them. With a single mode the selector renders a non-interactive label.

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

input/ModeSelector.tsx
import React, { memo, useCallback, useState } from 'react';
import { UnstyledButton } from '@mantine/core';
import { IconCheck, IconChevronDown } from '@tabler/icons-react';
import { cx } from '../utils/cx';
import { InputPopover } from './InputPopover';
import classes from './ModeSelector.module.css';

export type ModeOption = {
  id: string;
  label: string;
  /** Icon component, receives `className` for sizing */
  icon?: React.ComponentType<{ className?: string }>;
  description?: string;
};

export interface ModeSelectorProps {
  modes: ModeOption[];
  /** Controlled selected mode id */
  value?: string;
  /** Initial selected mode id in uncontrolled mode */
  defaultValue?: string;
  onChange?: (modeId: string) => void;
  className?: string;
  style?: React.CSSProperties;
}

/** Toolbar dropdown for switching agent modes (for example Agent / Plan). Renders a static label when only one mode is given. */
export const ModeSelector = memo(function ModeSelector({
  modes,
  value,
  defaultValue,
  onChange,
  className,
  style,
}: ModeSelectorProps) {
  const isControlled = value !== undefined;
  const [internalValue, setInternalValue] = useState(defaultValue);
  const activeId = isControlled ? value : internalValue;
  const activeMode = modes.find((m) => m.id === activeId) ?? modes[0];
  const [open, setOpen] = useState(false);

  const handleSelect = useCallback(
    (id: string) => {
      if (!isControlled) {
        setInternalValue(id);
      }
      onChange?.(id);
      setOpen(false);
    },
    [isControlled, onChange]
  );

  if (modes.length === 0) {
    return null;
  }
  const ActiveIcon = activeMode?.icon;
  const hasMultiple = modes.length > 1;

  const trigger = (
    <UnstyledButton
      component={hasMultiple ? 'button' : 'span'}
      className={cx(classes.trigger, className)}
      style={style}
      data-static={!hasMultiple || undefined}
      aria-label={hasMultiple ? 'Select mode' : undefined}
    >
      {ActiveIcon && <ActiveIcon className={classes.triggerIcon} />}
      <span className={classes.label}>{activeMode?.label}</span>
      {hasMultiple && <IconChevronDown size={12} className={classes.chevron} />}
    </UnstyledButton>
  );

  if (!hasMultiple) {
    return trigger;
  }

  return (
    <InputPopover open={open} onOpenChange={setOpen} side="top" align="start" trigger={trigger}>
      {modes.map((mode) => {
        const isActive = mode.id === activeMode?.id;
        const Icon = mode.icon;
        return (
          <UnstyledButton
            key={mode.id}
            onClick={() => handleSelect(mode.id)}
            className={classes.option}
            data-active={isActive || undefined}
          >
            {Icon && <Icon className={classes.optionIcon} />}
            <span className={classes.optionText}>
              <span className={classes.optionLabel}>{mode.label}</span>
              {mode.description && (
                <span className={classes.optionDescription}>{mode.description}</span>
              )}
            </span>
            {isActive && <IconCheck size={14} className={classes.check} />}
          </UnstyledButton>
        );
      })}
    </InputPopover>
  );
});

ModeSelector.displayName = 'ModeSelector';
API reference
Prop
Type
Required
modes
ModeOption[]
Yes
value
string
No
Controlled selected mode id
defaultValue
string
No
Initial selected mode id in uncontrolled mode
onChange
(modeId: string) => void
No
className
string
No
style
React.CSSProperties
No