import { type ProjectEntry, type ProviderKind } from "@t3tools/contracts";import { memo } from "react";import { type ComposerSlashCommand, type ComposerTriggerKind } from "../../composer-logic";import { BotIcon } from "lucide-react";import { cn } from "~/lib/utils";import { Badge } from "../ui/badge";import { Command, CommandItem, CommandList } from "../ui/command";import { VscodeEntryIcon } from "./VscodeEntryIcon"; 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: "model"; provider: ProviderKind; model: string; label: string; description: string; }; export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { items: ComposerCommandItem[]; resolvedTheme: "light" | "dark"; isLoading: boolean; triggerKind: ComposerTriggerKind | null; activeItemId: string | null; onHighlightedItemChange: (itemId: string | null) => void; onSelect: (item: ComposerCommandItem) => void;}) { return ( <Command mode="none" onItemHighlighted={(highlightedValue) => { props.onHighlightedItemChange( typeof highlightedValue === "string" ? highlightedValue : null, ); }} > <div className="relative overflow-hidden rounded-xl border border-border/80 bg-popover/96 shadow-lg/8 backdrop-blur-xs"> <CommandList className="max-h-64"> {props.items.map((item) => ( <ComposerCommandMenuItem key={item.id} item={item} resolvedTheme={props.resolvedTheme} isActive={props.activeItemId === item.id} onSelect={props.onSelect} /> ))} </CommandList> {props.items.length === 0 && ( <p className="px-3 py-2 text-muted-foreground/70 text-xs"> {props.isLoading ? "Searching workspace files..." : props.triggerKind === "path" ? "No matching files or folders." : "No matching command."} </p> )} </div> </Command> );}); const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: { item: ComposerCommandItem; resolvedTheme: "light" | "dark"; isActive: boolean; onSelect: (item: ComposerCommandItem) => void;}) { return ( <CommandItem value={props.item.id} className={cn( "cursor-pointer select-none gap-2", props.isActive && "bg-accent text-accent-foreground", )} onMouseDown={(event) => { event.preventDefault(); }} onClick={() => { props.onSelect(props.item); }} > {props.item.type === "path" ? ( <VscodeEntryIcon pathValue={props.item.path} kind={props.item.pathKind} theme={props.resolvedTheme} /> ) : null} {props.item.type === "slash-command" ? ( <BotIcon className="size-4 text-muted-foreground/80" /> ) : null} {props.item.type === "model" ? ( <Badge variant="outline" className="px-1.5 py-0 text-[10px]"> model </Badge> ) : null} <span className="flex min-w-0 items-center gap-1.5 truncate"> <span className="truncate">{props.item.label}</span> </span> <span className="truncate text-muted-foreground/70 text-xs">{props.item.description}</span> </CommandItem> );});
import { type ProjectEntry, type ProviderKind } from "@t3tools/contracts";
import { memo } from "react";
import { type ComposerSlashCommand, type ComposerTriggerKind } from "../../composer-logic";
import { BotIcon } from "lucide-react";
import { cn } from "~/lib/utils";
import { Badge } from "../ui/badge";
import { Command, CommandItem, CommandList } from "../ui/command";
import { VscodeEntryIcon } from "./VscodeEntryIcon";
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: "model";
provider: ProviderKind;
model: string;
label: string;
description: string;
};
export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
items: ComposerCommandItem[];
resolvedTheme: "light" | "dark";
isLoading: boolean;
triggerKind: ComposerTriggerKind | null;
activeItemId: string | null;
onHighlightedItemChange: (itemId: string | null) => void;
onSelect: (item: ComposerCommandItem) => void;
}) {
return (
<Command
mode="none"
onItemHighlighted={(highlightedValue) => {
props.onHighlightedItemChange(
typeof highlightedValue === "string" ? highlightedValue : null,
);
}}
>
<div className="relative overflow-hidden rounded-xl border border-border/80 bg-popover/96 shadow-lg/8 backdrop-blur-xs">
<CommandList className="max-h-64">
{props.items.map((item) => (
<ComposerCommandMenuItem
key={item.id}
item={item}
resolvedTheme={props.resolvedTheme}
isActive={props.activeItemId === item.id}
onSelect={props.onSelect}
/>
))}
</CommandList>
{props.items.length === 0 && (
<p className="px-3 py-2 text-muted-foreground/70 text-xs">
{props.isLoading
? "Searching workspace files..."
: props.triggerKind === "path"
? "No matching files or folders."
: "No matching command."}
</p>
)}
</div>
</Command>
);
});
const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: {
item: ComposerCommandItem;
resolvedTheme: "light" | "dark";
isActive: boolean;
onSelect: (item: ComposerCommandItem) => void;
}) {
return (
<CommandItem
value={props.item.id}
className={cn(
"cursor-pointer select-none gap-2",
props.isActive && "bg-accent text-accent-foreground",
)}
onMouseDown={(event) => {
event.preventDefault();
}}
onClick={() => {
props.onSelect(props.item);
}}
>
{props.item.type === "path" ? (
<VscodeEntryIcon
pathValue={props.item.path}
kind={props.item.pathKind}
theme={props.resolvedTheme}
/>
) : null}
{props.item.type === "slash-command" ? (
<BotIcon className="size-4 text-muted-foreground/80" />
) : null}
{props.item.type === "model" ? (
<Badge variant="outline" className="px-1.5 py-0 text-[10px]">
model
</Badge>
) : null}
<span className="flex min-w-0 items-center gap-1.5 truncate">
<span className="truncate">{props.item.label}</span>
</span>
<span className="truncate text-muted-foreground/70 text-xs">{props.item.description}</span>
</CommandItem>
);
});