@gram-ai/elements 1.21.3 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Chat/stories/ToolMentions.stories.d.ts +19 -0
- package/dist/components/assistant-ui/mentioned-tools-badges.d.ts +10 -0
- package/dist/components/assistant-ui/tool-mention-autocomplete.d.ts +12 -0
- package/dist/elements.cjs +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +1 -1
- package/dist/hooks/useToolMentions.d.ts +17 -0
- package/dist/{index-D5cSC22A.cjs → index-DSGCuihM.cjs} +47 -46
- package/dist/index-DSGCuihM.cjs.map +1 -0
- package/dist/{index-CKBUBkLd.js → index-DiaPGyWF.js} +11034 -10678
- package/dist/index-DiaPGyWF.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/lib/tool-mentions.d.ts +20 -0
- package/dist/{profiler-BY6U7KRN.cjs → profiler-Cr1UEa8B.cjs} +2 -2
- package/dist/{profiler-BY6U7KRN.cjs.map → profiler-Cr1UEa8B.cjs.map} +1 -1
- package/dist/{profiler-CRozVGVC.js → profiler-DL_h66-O.js} +2 -2
- package/dist/{profiler-CRozVGVC.js.map → profiler-DL_h66-O.js.map} +1 -1
- package/dist/{startRecording-DQXktVbB.js → startRecording-BW2zkd0-.js} +2 -2
- package/dist/{startRecording-DQXktVbB.js.map → startRecording-BW2zkd0-.js.map} +1 -1
- package/dist/{startRecording-Bi82O_dZ.cjs → startRecording-D8-ckYMS.cjs} +2 -2
- package/dist/{startRecording-Bi82O_dZ.cjs.map → startRecording-D8-ckYMS.cjs.map} +1 -1
- package/dist/types/index.d.ts +14 -0
- package/package.json +1 -1
- package/src/components/Chat/stories/ToolMentions.stories.tsx +62 -0
- package/src/components/assistant-ui/mentioned-tools-badges.tsx +105 -0
- package/src/components/assistant-ui/thread.tsx +106 -1
- package/src/components/assistant-ui/tool-mention-autocomplete.tsx +253 -0
- package/src/components/ui/tool-ui.tsx +1 -1
- package/src/contexts/ElementsProvider.tsx +2 -1
- package/src/hooks/useToolMentions.ts +93 -0
- package/src/index.ts +1 -0
- package/src/lib/tool-mentions.ts +106 -0
- package/src/types/index.ts +16 -0
- package/dist/index-CKBUBkLd.js.map +0 -1
- package/dist/index-D5cSC22A.cjs.map +0 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Wrench } from 'lucide-react'
|
|
3
|
+
import * as m from 'motion/react-m'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { useDensity } from '@/hooks/useDensity'
|
|
7
|
+
import { useRadius } from '@/hooks/useRadius'
|
|
8
|
+
import { EASE_OUT_QUINT } from '@/lib/easing'
|
|
9
|
+
import {
|
|
10
|
+
MentionableTool,
|
|
11
|
+
detectMentionContext,
|
|
12
|
+
filterToolsByQuery,
|
|
13
|
+
insertToolMention,
|
|
14
|
+
} from '@/lib/tool-mentions'
|
|
15
|
+
|
|
16
|
+
export interface ToolMentionAutocompleteProps {
|
|
17
|
+
tools: MentionableTool[]
|
|
18
|
+
value: string
|
|
19
|
+
cursorPosition: number
|
|
20
|
+
onValueChange: (value: string, cursorPosition: number) => void
|
|
21
|
+
textareaRef: React.RefObject<HTMLTextAreaElement | null>
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const ToolMentionAutocomplete: FC<ToolMentionAutocompleteProps> = ({
|
|
26
|
+
tools,
|
|
27
|
+
value,
|
|
28
|
+
cursorPosition,
|
|
29
|
+
onValueChange,
|
|
30
|
+
textareaRef,
|
|
31
|
+
className,
|
|
32
|
+
}) => {
|
|
33
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
34
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
35
|
+
const [filteredTools, setFilteredTools] = useState<MentionableTool[]>([])
|
|
36
|
+
const [mentionContext, setMentionContext] = useState<{
|
|
37
|
+
atPosition: number
|
|
38
|
+
query: string
|
|
39
|
+
} | null>(null)
|
|
40
|
+
|
|
41
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
42
|
+
const d = useDensity()
|
|
43
|
+
const r = useRadius()
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const context = detectMentionContext(value, cursorPosition)
|
|
47
|
+
|
|
48
|
+
if (context.isInMention && tools.length > 0) {
|
|
49
|
+
const filtered = filterToolsByQuery(tools, context.query)
|
|
50
|
+
setFilteredTools(filtered)
|
|
51
|
+
setIsVisible(filtered.length > 0)
|
|
52
|
+
setMentionContext({
|
|
53
|
+
atPosition: context.atPosition,
|
|
54
|
+
query: context.query,
|
|
55
|
+
})
|
|
56
|
+
setSelectedIndex(0)
|
|
57
|
+
} else {
|
|
58
|
+
setIsVisible(false)
|
|
59
|
+
setMentionContext(null)
|
|
60
|
+
}
|
|
61
|
+
}, [value, cursorPosition, tools])
|
|
62
|
+
|
|
63
|
+
const selectTool = useCallback(
|
|
64
|
+
(tool: MentionableTool) => {
|
|
65
|
+
if (!mentionContext) return
|
|
66
|
+
|
|
67
|
+
const result = insertToolMention(
|
|
68
|
+
value,
|
|
69
|
+
tool.name,
|
|
70
|
+
mentionContext.atPosition,
|
|
71
|
+
cursorPosition
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
onValueChange(result.text, result.cursorPosition)
|
|
75
|
+
setIsVisible(false)
|
|
76
|
+
},
|
|
77
|
+
[mentionContext, value, cursorPosition, onValueChange, textareaRef]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!isVisible) return
|
|
82
|
+
|
|
83
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
84
|
+
switch (e.key) {
|
|
85
|
+
case 'ArrowDown':
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
e.stopPropagation()
|
|
88
|
+
setSelectedIndex((prev) => (prev + 1) % filteredTools.length)
|
|
89
|
+
break
|
|
90
|
+
case 'ArrowUp':
|
|
91
|
+
e.preventDefault()
|
|
92
|
+
e.stopPropagation()
|
|
93
|
+
setSelectedIndex(
|
|
94
|
+
(prev) => (prev - 1 + filteredTools.length) % filteredTools.length
|
|
95
|
+
)
|
|
96
|
+
break
|
|
97
|
+
case 'Enter':
|
|
98
|
+
case 'Tab':
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
e.stopPropagation()
|
|
101
|
+
if (filteredTools[selectedIndex]) {
|
|
102
|
+
selectTool(filteredTools[selectedIndex])
|
|
103
|
+
}
|
|
104
|
+
break
|
|
105
|
+
case 'Escape':
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
e.stopPropagation()
|
|
108
|
+
setIsVisible(false)
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const textarea = textareaRef.current
|
|
114
|
+
if (textarea) {
|
|
115
|
+
textarea.addEventListener('keydown', handleKeyDown, { capture: true })
|
|
116
|
+
return () =>
|
|
117
|
+
textarea.removeEventListener('keydown', handleKeyDown, {
|
|
118
|
+
capture: true,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}, [isVisible, filteredTools, selectedIndex, selectTool, textareaRef])
|
|
122
|
+
|
|
123
|
+
// Scroll selected item into view
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!isVisible) return
|
|
126
|
+
const container = containerRef.current
|
|
127
|
+
if (!container) return
|
|
128
|
+
|
|
129
|
+
const selectedItem = container.querySelector(
|
|
130
|
+
`[data-index="${selectedIndex}"]`
|
|
131
|
+
) as HTMLElement
|
|
132
|
+
if (selectedItem) {
|
|
133
|
+
selectedItem.scrollIntoView({ block: 'nearest' })
|
|
134
|
+
}
|
|
135
|
+
}, [selectedIndex, isVisible])
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!isVisible) return
|
|
139
|
+
|
|
140
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
141
|
+
if (
|
|
142
|
+
containerRef.current &&
|
|
143
|
+
!containerRef.current.contains(e.target as Node) &&
|
|
144
|
+
textareaRef.current &&
|
|
145
|
+
!textareaRef.current.contains(e.target as Node)
|
|
146
|
+
) {
|
|
147
|
+
setIsVisible(false)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
152
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
153
|
+
}, [isVisible, textareaRef])
|
|
154
|
+
|
|
155
|
+
// When autocomplete is visible, modify composer styles and position autocomplete to match composer width
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const textarea = textareaRef.current
|
|
158
|
+
if (!textarea) return
|
|
159
|
+
|
|
160
|
+
const composer = textarea.closest('.aui-composer-root') as HTMLElement
|
|
161
|
+
if (!composer) return
|
|
162
|
+
|
|
163
|
+
const updateStyles = () => {
|
|
164
|
+
const autocomplete = containerRef.current
|
|
165
|
+
if (!autocomplete) return
|
|
166
|
+
|
|
167
|
+
if (isVisible) {
|
|
168
|
+
// Modify composer to connect with autocomplete
|
|
169
|
+
composer.style.borderTopColor = 'var(--ring)'
|
|
170
|
+
composer.style.borderTopLeftRadius = '0'
|
|
171
|
+
composer.style.borderTopRightRadius = '0'
|
|
172
|
+
|
|
173
|
+
// Position autocomplete to match composer width
|
|
174
|
+
const composerRect = composer.getBoundingClientRect()
|
|
175
|
+
const autocompleteParent = autocomplete.offsetParent as HTMLElement
|
|
176
|
+
if (autocompleteParent) {
|
|
177
|
+
const parentRect = autocompleteParent.getBoundingClientRect()
|
|
178
|
+
autocomplete.style.left = `${composerRect.left - parentRect.left}px`
|
|
179
|
+
autocomplete.style.right = 'auto'
|
|
180
|
+
autocomplete.style.width = `${composerRect.width}px`
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (isVisible) {
|
|
186
|
+
// Use requestAnimationFrame to ensure DOM is updated
|
|
187
|
+
requestAnimationFrame(updateStyles)
|
|
188
|
+
} else {
|
|
189
|
+
composer.style.borderTopColor = ''
|
|
190
|
+
composer.style.borderTopLeftRadius = ''
|
|
191
|
+
composer.style.borderTopRightRadius = ''
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return () => {
|
|
195
|
+
composer.style.borderTopColor = ''
|
|
196
|
+
composer.style.borderTopLeftRadius = ''
|
|
197
|
+
composer.style.borderTopRightRadius = ''
|
|
198
|
+
}
|
|
199
|
+
}, [isVisible, textareaRef])
|
|
200
|
+
|
|
201
|
+
if (!isVisible || filteredTools.length === 0) {
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<m.div
|
|
207
|
+
ref={containerRef}
|
|
208
|
+
initial={{ opacity: 0, y: 8 }}
|
|
209
|
+
animate={{ opacity: 1, y: 0 }}
|
|
210
|
+
exit={{ opacity: 0, y: 8 }}
|
|
211
|
+
transition={{ duration: 0.15, ease: EASE_OUT_QUINT }}
|
|
212
|
+
className={cn(
|
|
213
|
+
'aui-tool-mention-autocomplete border-ring bg-background absolute bottom-full z-50 max-h-[220px] overflow-clip overflow-y-auto overscroll-contain rounded-br-none! rounded-bl-none! border border-b-0 shadow-xs',
|
|
214
|
+
r('xl'),
|
|
215
|
+
className
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
<div className="flex flex-col gap-1">
|
|
219
|
+
{filteredTools.map((tool, index) => (
|
|
220
|
+
<button
|
|
221
|
+
key={tool.id}
|
|
222
|
+
type="button"
|
|
223
|
+
data-index={index}
|
|
224
|
+
className={cn(
|
|
225
|
+
'aui-tool-mention-item flex w-full items-center gap-2 text-left transition-colors',
|
|
226
|
+
d('p-sm'),
|
|
227
|
+
'hover:bg-accent hover:text-accent-foreground',
|
|
228
|
+
index === selectedIndex && 'bg-accent text-accent-foreground'
|
|
229
|
+
)}
|
|
230
|
+
onClick={(e) => {
|
|
231
|
+
e.preventDefault()
|
|
232
|
+
e.stopPropagation()
|
|
233
|
+
selectTool(tool)
|
|
234
|
+
}}
|
|
235
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
236
|
+
>
|
|
237
|
+
<Wrench className="mt-0.5 size-4 flex-shrink-0 opacity-50" />
|
|
238
|
+
<div className="min-w-0 flex-1">
|
|
239
|
+
<div className="text-sm font-medium">{tool.name}</div>
|
|
240
|
+
{tool.description && (
|
|
241
|
+
<div className="text-muted-foreground line-clamp-2 text-xs">
|
|
242
|
+
{tool.description}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
</button>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
</m.div>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export default ToolMentionAutocomplete
|
|
@@ -322,7 +322,7 @@ function ToolUISection({
|
|
|
322
322
|
<div data-slot="tool-ui-section" className="border-border border-t">
|
|
323
323
|
<button
|
|
324
324
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
325
|
-
className="hover:bg-accent/50 flex w-full cursor-pointer items-center justify-between px-
|
|
325
|
+
className="hover:bg-accent/50 flex w-full cursor-pointer items-center justify-between px-5 py-2.5 text-left transition-colors"
|
|
326
326
|
>
|
|
327
327
|
<span className="text-muted-foreground text-sm">{title}</span>
|
|
328
328
|
<div className="flex items-center gap-1">
|
|
@@ -349,8 +349,9 @@ const ElementsProviderWithApproval = ({
|
|
|
349
349
|
isOpen: isOpen ?? false,
|
|
350
350
|
setIsOpen,
|
|
351
351
|
plugins,
|
|
352
|
+
mcpTools,
|
|
352
353
|
}),
|
|
353
|
-
[config, model, isExpanded, isOpen, plugins]
|
|
354
|
+
[config, model, isExpanded, isOpen, plugins, mcpTools]
|
|
354
355
|
)
|
|
355
356
|
|
|
356
357
|
const frontendTools = config.tools?.frontendTools ?? {}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { useAssistantApi, useAssistantState } from '@assistant-ui/react'
|
|
3
|
+
import {
|
|
4
|
+
MentionableTool,
|
|
5
|
+
parseMentionedTools,
|
|
6
|
+
removeToolMention,
|
|
7
|
+
toolSetToMentionableTools,
|
|
8
|
+
} from '@/lib/tool-mentions'
|
|
9
|
+
|
|
10
|
+
export interface UseToolMentionsOptions {
|
|
11
|
+
tools: Record<string, unknown> | undefined
|
|
12
|
+
enabled?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseToolMentionsReturn {
|
|
16
|
+
mentionableTools: MentionableTool[]
|
|
17
|
+
mentionedToolIds: string[]
|
|
18
|
+
value: string
|
|
19
|
+
cursorPosition: number
|
|
20
|
+
textareaRef: React.RefObject<HTMLTextAreaElement | null>
|
|
21
|
+
updateCursorPosition: () => void
|
|
22
|
+
handleAutocompleteChange: (value: string, cursorPosition: number) => void
|
|
23
|
+
removeMention: (toolId: string) => void
|
|
24
|
+
isActive: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useToolMentions({
|
|
28
|
+
tools,
|
|
29
|
+
enabled = true,
|
|
30
|
+
}: UseToolMentionsOptions): UseToolMentionsReturn {
|
|
31
|
+
const [cursorPosition, setCursorPosition] = useState(0)
|
|
32
|
+
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
|
|
33
|
+
const api = useAssistantApi()
|
|
34
|
+
const composerText = useAssistantState(({ composer }) => composer.text)
|
|
35
|
+
|
|
36
|
+
const mentionableTools = useMemo(
|
|
37
|
+
() => toolSetToMentionableTools(tools),
|
|
38
|
+
[tools]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const mentionedToolIds = useMemo(
|
|
42
|
+
() => (enabled ? parseMentionedTools(composerText, tools) : []),
|
|
43
|
+
[composerText, tools, enabled]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const updateCursorPosition = useCallback(() => {
|
|
47
|
+
const textarea = textareaRef.current
|
|
48
|
+
if (textarea) {
|
|
49
|
+
setCursorPosition(textarea.selectionStart)
|
|
50
|
+
}
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
const handleAutocompleteChange = useCallback(
|
|
54
|
+
(newValue: string, newCursorPosition: number) => {
|
|
55
|
+
api.composer().setText(newValue)
|
|
56
|
+
setCursorPosition(newCursorPosition)
|
|
57
|
+
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
const textarea = textareaRef.current
|
|
60
|
+
if (textarea) {
|
|
61
|
+
textarea.focus()
|
|
62
|
+
textarea.setSelectionRange(newCursorPosition, newCursorPosition)
|
|
63
|
+
}
|
|
64
|
+
}, 0)
|
|
65
|
+
},
|
|
66
|
+
[api]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const removeMention = useCallback(
|
|
70
|
+
(toolId: string) => {
|
|
71
|
+
const tool = mentionableTools.find((t) => t.id === toolId)
|
|
72
|
+
if (tool) {
|
|
73
|
+
const newValue = removeToolMention(composerText, tool.name)
|
|
74
|
+
api.composer().setText(newValue)
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
[composerText, mentionableTools, api]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const isActive = enabled && mentionableTools.length > 0
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
mentionableTools,
|
|
84
|
+
mentionedToolIds,
|
|
85
|
+
value: composerText,
|
|
86
|
+
cursorPosition,
|
|
87
|
+
textareaRef,
|
|
88
|
+
updateCursorPosition,
|
|
89
|
+
handleAutocompleteChange,
|
|
90
|
+
removeMention,
|
|
91
|
+
isActive,
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export type ToolRecord = Record<string, unknown> | undefined
|
|
2
|
+
|
|
3
|
+
export interface MentionableTool {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
description?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MentionContext {
|
|
10
|
+
isInMention: boolean
|
|
11
|
+
query: string
|
|
12
|
+
atPosition: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MENTION_PATTERN = /@(\w+)/g
|
|
16
|
+
|
|
17
|
+
export function toolSetToMentionableTools(
|
|
18
|
+
tools: ToolRecord
|
|
19
|
+
): MentionableTool[] {
|
|
20
|
+
if (!tools) return []
|
|
21
|
+
|
|
22
|
+
return Object.entries(tools).map(([name, tool]) => ({
|
|
23
|
+
id: name,
|
|
24
|
+
name,
|
|
25
|
+
description:
|
|
26
|
+
typeof tool === 'object' && tool !== null && 'description' in tool
|
|
27
|
+
? String((tool as { description?: unknown }).description ?? '')
|
|
28
|
+
: undefined,
|
|
29
|
+
}))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseMentionedTools(text: string, tools: ToolRecord): string[] {
|
|
33
|
+
if (!tools || !text) return []
|
|
34
|
+
|
|
35
|
+
const toolNames = Object.keys(tools)
|
|
36
|
+
const mentions: string[] = []
|
|
37
|
+
let match: RegExpExecArray | null
|
|
38
|
+
|
|
39
|
+
MENTION_PATTERN.lastIndex = 0
|
|
40
|
+
while ((match = MENTION_PATTERN.exec(text)) !== null) {
|
|
41
|
+
mentions.push(match[1].toLowerCase())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const matchedToolIds = toolNames.filter((name) =>
|
|
45
|
+
mentions.includes(name.toLowerCase())
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return [...new Set(matchedToolIds)]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function detectMentionContext(
|
|
52
|
+
text: string,
|
|
53
|
+
cursorPosition: number
|
|
54
|
+
): MentionContext {
|
|
55
|
+
const textBeforeCursor = text.slice(0, cursorPosition)
|
|
56
|
+
const lastAtSymbol = textBeforeCursor.lastIndexOf('@')
|
|
57
|
+
|
|
58
|
+
if (lastAtSymbol === -1) {
|
|
59
|
+
return { isInMention: false, query: '', atPosition: -1 }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const textAfterAt = textBeforeCursor.slice(lastAtSymbol + 1)
|
|
63
|
+
|
|
64
|
+
if (textAfterAt.includes(' ') || textAfterAt.includes('\n')) {
|
|
65
|
+
return { isInMention: false, query: '', atPosition: -1 }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
isInMention: true,
|
|
70
|
+
query: textAfterAt.toLowerCase(),
|
|
71
|
+
atPosition: lastAtSymbol,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function filterToolsByQuery(
|
|
76
|
+
tools: MentionableTool[],
|
|
77
|
+
query: string
|
|
78
|
+
): MentionableTool[] {
|
|
79
|
+
if (!query) return tools
|
|
80
|
+
|
|
81
|
+
const queryLower = query.toLowerCase()
|
|
82
|
+
|
|
83
|
+
return tools.filter((tool) => {
|
|
84
|
+
const nameMatch = tool.name.toLowerCase().includes(queryLower)
|
|
85
|
+
const descMatch = tool.description?.toLowerCase().includes(queryLower)
|
|
86
|
+
return nameMatch || descMatch
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function insertToolMention(
|
|
91
|
+
text: string,
|
|
92
|
+
toolName: string,
|
|
93
|
+
atPosition: number,
|
|
94
|
+
cursorPosition: number
|
|
95
|
+
): { text: string; cursorPosition: number } {
|
|
96
|
+
const beforeMention = text.slice(0, atPosition)
|
|
97
|
+
const afterCursor = text.slice(cursorPosition)
|
|
98
|
+
const newText = `${beforeMention}@${toolName} ${afterCursor}`
|
|
99
|
+
const newCursorPosition = atPosition + toolName.length + 2
|
|
100
|
+
return { text: newText, cursorPosition: newCursorPosition }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function removeToolMention(text: string, toolName: string): string {
|
|
104
|
+
const pattern = new RegExp(`@${toolName}\\s?`, 'gi')
|
|
105
|
+
return text.replace(pattern, '')
|
|
106
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -754,6 +754,13 @@ export interface ComposerConfig {
|
|
|
754
754
|
* @default true
|
|
755
755
|
*/
|
|
756
756
|
attachments?: boolean | AttachmentsConfig
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Configuration for @tool mentions in the composer.
|
|
760
|
+
* Set to `false` to disable, `true` for defaults, or an object for fine-grained control.
|
|
761
|
+
* @default true
|
|
762
|
+
*/
|
|
763
|
+
toolMentions?: boolean | ToolMentionsConfig
|
|
757
764
|
}
|
|
758
765
|
|
|
759
766
|
/**
|
|
@@ -781,6 +788,14 @@ export interface AttachmentsConfig {
|
|
|
781
788
|
maxSize?: number
|
|
782
789
|
}
|
|
783
790
|
|
|
791
|
+
export interface ToolMentionsConfig {
|
|
792
|
+
/** @default true */
|
|
793
|
+
enabled?: boolean
|
|
794
|
+
/** @default 10 */
|
|
795
|
+
maxSuggestions?: number
|
|
796
|
+
placeholder?: string
|
|
797
|
+
}
|
|
798
|
+
|
|
784
799
|
export interface SidecarConfig extends ExpandableConfig {
|
|
785
800
|
/**
|
|
786
801
|
* The title displayed in the sidecar header.
|
|
@@ -829,4 +844,5 @@ export type ElementsContextType = {
|
|
|
829
844
|
isOpen: boolean
|
|
830
845
|
setIsOpen: (isOpen: boolean) => void
|
|
831
846
|
plugins: Plugin[]
|
|
847
|
+
mcpTools: Record<string, unknown> | undefined
|
|
832
848
|
}
|