258 lines · 8.4 KB · TypeScript
RawDownload
import {  type ProjectEntry,  type ProviderDriverKind,  type ServerProviderSkill,  type ServerProviderSlashCommand,} from "@t3tools/contracts";import { BotIcon } from "lucide-react";import { memo, useLayoutEffect, useMemo, useRef } from "react"; import { type ComposerSlashCommand, type ComposerTriggerKind } from "../../composer-logic";import { formatProviderSkillInstallSource } from "~/providerSkillPresentation";import { cn } from "~/lib/utils";import {  Command,  CommandGroup,  CommandGroupLabel,  CommandItem,  CommandList,  CommandSeparator,} from "../ui/command";import { PierreEntryIcon } from "./PierreEntryIcon"; export type ComposerCommandItem =  | {      id: string;      type: "path";      path: string;      pathKind: ProjectEntry["kind"];      label: string;      description: string;    }  | {      id: string;      type: "slash-command";      command: ComposerSlashCommand;      label: string;      description: string;    }  | {      id: string;      type: "provider-slash-command";      provider: ProviderDriverKind;      command: ServerProviderSlashCommand;      label: string;      description: string;    }  | {      id: string;      type: "skill";      provider: ProviderDriverKind;      skill: ServerProviderSkill;      label: string;      description: string;    }; type ComposerCommandGroup = {  id: string;  label: string | null;  items: ComposerCommandItem[];}; function SkillGlyph(props: { className?: string }) {  return (    <svg      viewBox="0 0 24 24"      fill="none"      stroke="currentColor"      strokeWidth="1.85"      strokeLinecap="round"      strokeLinejoin="round"      className={props.className}      aria-hidden="true"    >      <path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />      <path d="m3.3 7 8.7 5 8.7-5" />      <path d="M12 22V12" />    </svg>  );} function groupCommandItems(  items: ComposerCommandItem[],  triggerKind: ComposerTriggerKind | null,  groupSlashCommandSections: boolean,): ComposerCommandGroup[] {  if (triggerKind === "skill") {    return items.length > 0 ? [{ id: "skills", label: "Skills", items }] : [];  }  if (triggerKind !== "slash-command" || !groupSlashCommandSections) {    return [{ id: "default", label: null, items }];  }   const builtInItems = items.filter((item) => item.type === "slash-command");  const providerItems = items.filter((item) => item.type === "provider-slash-command");   const groups: ComposerCommandGroup[] = [];  if (builtInItems.length > 0) {    groups.push({ id: "built-in", label: "Built-in", items: builtInItems });  }  if (providerItems.length > 0) {    groups.push({ id: "provider", label: "Provider", items: providerItems });  }  return groups;} export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {  items: ComposerCommandItem[];  resolvedTheme: "light" | "dark";  isLoading: boolean;  triggerKind: ComposerTriggerKind | null;  groupSlashCommandSections?: boolean;  emptyStateText?: string;  activeItemId: string | null;  onHighlightedItemChange: (itemId: string | null) => void;  onSelect: (item: ComposerCommandItem) => void;}) {  const listRef = useRef<HTMLDivElement>(null);  const groups = useMemo(    () =>      groupCommandItems(props.items, props.triggerKind, props.groupSlashCommandSections ?? true),    [props.groupSlashCommandSections, props.items, props.triggerKind],  );   useLayoutEffect(() => {    if (!props.activeItemId || !listRef.current) return;    const el = listRef.current.querySelector<HTMLElement>(      `[data-composer-item-id="${CSS.escape(props.activeItemId)}"]`,    );    el?.scrollIntoView({ block: "nearest" });  }, [props.activeItemId]);   return (    <Command      autoHighlight={false}      mode="none"      onItemHighlighted={(highlightedValue) => {        props.onHighlightedItemChange(          typeof highlightedValue === "string" ? highlightedValue : null,        );      }}    >      <div ref={listRef} className="dropdown-glass relative w-full overflow-hidden rounded-[20px]">        {props.items.length > 0 ? (          <CommandList className="max-h-72">            {groups.map((group, groupIndex) => (              <div key={group.id}>                {groupIndex > 0 ? <CommandSeparator className="my-0.5" /> : null}                <CommandGroup>                  {group.label ? (                    <CommandGroupLabel className="px-3 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground/55">                      {group.label}                    </CommandGroupLabel>                  ) : null}                  {group.items.map((item) => (                    <ComposerCommandMenuItem                      key={item.id}                      item={item}                      resolvedTheme={props.resolvedTheme}                      isActive={props.activeItemId === item.id}                      onHighlight={props.onHighlightedItemChange}                      onSelect={props.onSelect}                    />                  ))}                </CommandGroup>              </div>            ))}          </CommandList>        ) : (          <div className="px-5 py-3.5">            {props.triggerKind === "skill" ? (              <CommandGroup>                <CommandGroupLabel className="px-0 pt-0 pb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground/55">                  Skills                </CommandGroupLabel>                <p className="text-muted-foreground/70 text-xs">                  {props.isLoading                    ? "Searching workspace skills..."                    : (props.emptyStateText ??                      "No skills found. Try / to browse provider commands.")}                </p>              </CommandGroup>            ) : (              <p className="text-muted-foreground/70 text-xs">                {props.isLoading                  ? "Searching workspace files..."                  : (props.emptyStateText ??                    (props.triggerKind === "path"                      ? "No matching files or folders."                      : "No matching command."))}              </p>            )}          </div>        )}      </div>    </Command>  );}); const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: {  item: ComposerCommandItem;  resolvedTheme: "light" | "dark";  isActive: boolean;  onHighlight: (itemId: string | null) => void;  onSelect: (item: ComposerCommandItem) => void;}) {  const skillSourceLabel =    props.item.type === "skill" ? formatProviderSkillInstallSource(props.item.skill) : null;   return (    <CommandItem      value={props.item.id}      data-composer-item-id={props.item.id}      className={cn(        "cursor-pointer select-none gap-2 hover:bg-transparent hover:text-inherit data-highlighted:bg-transparent data-highlighted:text-inherit",        props.isActive && "bg-accent! text-accent-foreground!",      )}      onMouseMove={() => {        if (!props.isActive) props.onHighlight(props.item.id);      }}      onMouseDown={(event) => {        event.preventDefault();      }}      onClick={() => {        props.onSelect(props.item);      }}    >      {props.item.type === "path" ? (        <PierreEntryIcon          pathValue={props.item.path}          kind={props.item.pathKind}          theme={props.resolvedTheme}        />      ) : null}      {props.item.type === "slash-command" ? (        <BotIcon className="size-4 shrink-0 text-muted-foreground/80" />      ) : null}      {props.item.type === "provider-slash-command" ? (        <span className="inline-flex size-4 shrink-0 items-center justify-center text-muted-foreground/80">          <SkillGlyph className="size-3.5" />        </span>      ) : null}      {props.item.type === "skill" ? (        <span className="inline-flex size-4 shrink-0 items-center justify-center text-muted-foreground/80">          <SkillGlyph className="size-3.5" />        </span>      ) : null}      <span className="flex min-w-0 flex-1 items-center gap-2">        <span className="shrink-0">{props.item.label}</span>        <span className="min-w-0 flex-1 truncate text-muted-foreground/70 text-xs">          {props.item.description}        </span>      </span>      {skillSourceLabel ? (        <span className="shrink-0 pl-2 text-muted-foreground/70 text-xs">{skillSourceLabel}</span>      ) : null}    </CommandItem>  );});