-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathComposerCommandMenu.tsx
More file actions
127 lines (124 loc) · 3.84 KB
/
ComposerCommandMenu.tsx
File metadata and controls
127 lines (124 loc) · 3.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { type ProjectEntry, type ModelSlug, type ProviderKind } from "@t3tools/contracts";
import { memo, useEffect, useRef } 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: ModelSlug;
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;
}) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (props.isActive) {
ref.current?.scrollIntoView({ block: "nearest" });
}
}, [props.isActive]);
return (
<CommandItem
ref={ref}
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>
);
});