mezame 0.8.42

An ACP client that bridges a local agent (Kiro CLI, Claude Agent CLI, Gemini CLI, Codex, ...) to a browser UI over WebSockets.
import { CheckIcon, ChevronDownIcon } from 'lucide-react';
import type { ReactNode } from 'react';
import { Button } from '@/components/ui/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { mezameActions } from '@/hooks/useMezame';
import { cn } from '@/lib/utils';
import type { Session } from '@/types';

// Tab-level selectors for the agent "mode" (really Kiro agent variants
// like kiro_default / kiro_planner / kiro_guide) and the underlying
// model. Both are session-scoped and disabled while the session is busy
// — Kiro doesn't guarantee mid-turn mode/model switches behave sensibly.

type Props = {
  session: Session | null;
  /** `row` packs both pickers side-by-side (legacy). `stack` places
   * them vertically so they fit next to a tall textarea without
   * stealing width. */
  layout?: 'row' | 'stack';
};

const triggerClass = cn(
  'h-7 gap-1.5 rounded-md border border-border bg-card px-2 text-[11px] text-foreground',
  'hover:text-foreground hover:bg-accent',
  'data-[state=open]:bg-accent data-[state=open]:text-foreground'
);

const itemClass = 'gap-2 py-1.5 text-xs';

const Picker = <T extends { id: string; label: string; description?: string }>({
  label,
  options,
  currentId,
  onPick,
  disabled,
  emptyLabel,
  triggerLabel
}: {
  label: string;
  options: T[];
  currentId: string | null;
  onPick: (id: string) => void;
  disabled: boolean;
  emptyLabel: string;
  triggerLabel: ReactNode;
}) => (
  <DropdownMenu>
    <Tooltip>
      <TooltipTrigger asChild>
        <DropdownMenuTrigger asChild disabled={disabled || options.length === 0}>
          <Button variant="ghost" size="sm" className={triggerClass}>
            <span className="text-[color:var(--primary)]">{label}:</span>
            <span className="max-w-[10rem] truncate">
              {options.length === 0 ? emptyLabel : triggerLabel}
            </span>
            <ChevronDownIcon className="size-3 shrink-0 opacity-60" />
          </Button>
        </DropdownMenuTrigger>
      </TooltipTrigger>
      <TooltipContent side="top">{label}</TooltipContent>
    </Tooltip>
    <DropdownMenuContent align="start" className="max-h-[60vh] overflow-y-auto">
      <DropdownMenuLabel>{label}</DropdownMenuLabel>
      <DropdownMenuSeparator />
      {options.length === 0 ? (
        <div className="px-2 py-1.5 text-xs text-muted-foreground">{emptyLabel}</div>
      ) : (
        options.map((opt) => {
          const active = opt.id === currentId;
          return (
            <DropdownMenuItem key={opt.id} className={itemClass} onSelect={() => onPick(opt.id)}>
              <CheckIcon
                className={cn('mt-0.5 size-3 shrink-0', active ? 'opacity-100' : 'opacity-0')}
              />
              <div className="min-w-0 flex-1">
                <div className="font-medium text-foreground">{opt.label}</div>
                {opt.description && (
                  <div className="text-[11px] text-muted-foreground">{opt.description}</div>
                )}
              </div>
            </DropdownMenuItem>
          );
        })
      )}
    </DropdownMenuContent>
  </DropdownMenu>
);

export const ModeModelSelectors = ({ session, layout = 'row' }: Props) => {
  if (!session) {
    return null;
  }
  // Hide the group entirely when the agent didn't advertise any modes
  // or models. Non-Kiro agents often don't, and the empty "none"
  // dropdowns add noise.
  if (session.modes.length === 0 && session.models.length === 0) {
    return null;
  }
  const busy = session.busy;

  const modeOptions = session.modes.map((m) => ({
    id: m.id,
    label: m.name || m.id,
    description: m.description
  }));
  const modelOptions = session.models.map((m) => ({
    id: m.modelId,
    label: m.name || m.modelId,
    description: m.description
  }));

  const currentMode = session.modes.find((m) => m.id === session.currentModeId);
  const currentModel = session.models.find((m) => m.modelId === session.currentModelId);

  const containerClass =
    layout === 'stack' ? 'flex flex-col gap-1.5 w-40' : 'flex items-center gap-1.5 flex-wrap justify-end';

  return (
    <div className={containerClass}>
      <Picker
        label="Agent"
        options={modeOptions}
        currentId={session.currentModeId}
        onPick={mezameActions.setMode}
        disabled={busy}
        emptyLabel="none"
        triggerLabel={currentMode?.name || currentMode?.id || '—'}
      />
      <Picker
        label="Model"
        options={modelOptions}
        currentId={session.currentModelId}
        onPick={mezameActions.setModel}
        disabled={busy}
        emptyLabel="none"
        triggerLabel={currentModel?.name || currentModel?.modelId || '—'}
      />
    </div>
  );
};