import { resolveProviderSkillSourceKind, type ProviderSkillSourceKind, } from "@t3tools/client-runtime/providerSkills"; import type { ServerProviderSkill, ServerProviderSlashCommand } from "@t3tools/contracts"; import type { ComposerTriggerKind } from "@t3tools/shared/composerTrigger"; import { memo } from "react"; import { Pressable, ScrollView, StyleSheet, View, type ViewStyle } from "react-native"; import { SymbolView, type AppSymbolName } from "../../components/AppSymbol"; import { AppText as Text } from "../../components/AppText"; import { GlassSurface } from "../../components/GlassSurface"; import { PierreEntryIcon } from "../../components/PierreEntryIcon"; export type ComposerCommandItem = | { readonly id: string; readonly type: "path"; readonly path: string; readonly kind: "file" | "directory"; readonly label: string; readonly description: string; } | { readonly id: string; readonly type: "slash-command"; readonly command: string; readonly label: string; readonly description: string; } | { readonly id: string; readonly type: "provider-slash-command"; readonly command: ServerProviderSlashCommand; readonly label: string; readonly description: string; } | { readonly id: string; readonly type: "skill"; readonly skill: ServerProviderSkill; readonly label: string; readonly description: string; }; interface ComposerCommandPopoverProps { readonly items: ReadonlyArray; readonly triggerKind: ComposerTriggerKind | null; readonly isLoading: boolean; readonly onSelect: (item: ComposerCommandItem) => void; } function PopoverSurface(props: { readonly children: React.ReactNode; readonly style?: ViewStyle }) { const baseStyle: ViewStyle = { borderRadius: 16, overflow: "hidden", ...props.style, }; return ( {props.children} ); } const SKILL_SOURCE_SYMBOL_BY_KIND: Record = { app: "square.grid.2x2", repo: "folder", project: "folder", personal: "person.crop.circle", system: "gearshape", other: "cube", }; function itemIcon(item: ComposerCommandItem): AppSymbolName | null { switch (item.type) { case "slash-command": case "provider-slash-command": return "terminal"; case "skill": return SKILL_SOURCE_SYMBOL_BY_KIND[resolveProviderSkillSourceKind(item.skill)]; case "path": return null; } } function groupLabel(triggerKind: ComposerTriggerKind | null): string | null { switch (triggerKind) { case "slash-command": return "Commands"; case "skill": return "Skills"; case "path": return "Files"; default: return null; } } function emptyText(triggerKind: ComposerTriggerKind | null, isLoading: boolean): string { if (isLoading) { return triggerKind === "path" ? "Searching files…" : "Loading…"; } switch (triggerKind) { case "path": return "No matching files or folders."; case "skill": return "No skills found."; case "slash-command": return "No matching commands."; default: return "No results."; } } const CommandRow = memo(function CommandRow(props: { readonly item: ComposerCommandItem; readonly onPress: () => void; readonly isLast: boolean; readonly isSlashSkill: boolean; }) { const iconName = itemIcon(props.item); return ( {props.item.type === "path" ? ( ) : iconName ? ( ) : null} {props.isSlashSkill && props.item.type === "skill" ? ( <> skill: {props.item.skill.name} ) : ( props.item.label )} {props.item.description ? ( {props.item.description} ) : null} ); }); export const ComposerCommandPopover = memo(function ComposerCommandPopover( props: ComposerCommandPopoverProps, ) { const label = groupLabel(props.triggerKind); return ( {label ? ( {label} ) : null} {props.items.length > 0 ? ( {props.items.map((item, index) => ( props.onSelect(item)} isLast={index === props.items.length - 1} isSlashSkill={props.triggerKind === "slash-command" && item.type === "skill"} /> ))} ) : ( {emptyText(props.triggerKind, props.isLoading)} )} ); });