@gram-ai/elements 1.21.2 → 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/StyleIsolation.stories.d.ts +6 -0
- package/dist/components/Chat/stories/Theme.stories.d.ts +8 -0
- package/dist/components/Chat/stories/ToolMentions.stories.d.ts +19 -0
- package/dist/components/ChatHistory.d.ts +5 -0
- package/dist/components/ShadowRoot.d.ts +8 -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/embedded.d.ts +0 -0
- package/dist/hooks/useGramThreadListAdapter.d.ts +7 -0
- package/dist/hooks/useMCPTools.d.ts +2 -1
- package/dist/hooks/useToolMentions.d.ts +17 -0
- package/dist/index-DSGCuihM.cjs +145 -0
- package/dist/index-DSGCuihM.cjs.map +1 -0
- package/dist/index-DiaPGyWF.js +38113 -0
- package/dist/index-DiaPGyWF.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/lib/tool-mentions.d.ts +20 -0
- package/dist/{profiler-DPH9zydw.cjs → profiler-Cr1UEa8B.cjs} +2 -2
- package/dist/{profiler-DPH9zydw.cjs.map → profiler-Cr1UEa8B.cjs.map} +1 -1
- package/dist/{profiler-D_HNXmyv.js → profiler-DL_h66-O.js} +2 -2
- package/dist/{profiler-D_HNXmyv.js.map → profiler-DL_h66-O.js.map} +1 -1
- package/dist/{startRecording-DOMzQAAr.js → startRecording-BW2zkd0-.js} +2 -2
- package/dist/{startRecording-DOMzQAAr.js.map → startRecording-BW2zkd0-.js.map} +1 -1
- package/dist/{startRecording-B97e4Mlu.cjs → startRecording-D8-ckYMS.cjs} +2 -2
- package/dist/{startRecording-B97e4Mlu.cjs.map → startRecording-D8-ckYMS.cjs.map} +1 -1
- package/dist/types/index.d.ts +20 -0
- package/package.json +13 -4
- package/src/components/Chat/index.tsx +4 -13
- package/src/components/Chat/stories/StyleIsolation.stories.tsx +41 -0
- package/src/components/Chat/stories/Theme.stories.tsx +74 -0
- package/src/components/Chat/stories/ToolApproval.stories.tsx +0 -1
- package/src/components/Chat/stories/ToolMentions.stories.tsx +62 -0
- package/src/components/ChatHistory.tsx +16 -0
- package/src/components/ShadowRoot.tsx +90 -0
- package/src/components/assistant-ui/markdown-text.tsx +0 -2
- package/src/components/assistant-ui/mentioned-tools-badges.tsx +105 -0
- package/src/components/assistant-ui/thread-list.tsx +1 -3
- 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 +67 -10
- package/src/embedded.ts +3 -0
- package/src/global.css +45 -0
- package/src/hooks/useGramThreadListAdapter.tsx +90 -9
- package/src/hooks/useMCPTools.ts +4 -1
- package/src/hooks/useToolMentions.ts +93 -0
- package/src/index.ts +2 -1
- package/src/lib/tool-mentions.ts +106 -0
- package/src/types/index.ts +23 -0
- package/src/vite-env.d.ts +5 -0
- package/dist/index-BVvrv2G3.cjs +0 -169
- package/dist/index-BVvrv2G3.cjs.map +0 -1
- package/dist/index-OU3wjArm.js +0 -54670
- package/dist/index-OU3wjArm.js.map +0 -1
package/src/global.css
CHANGED
|
@@ -247,3 +247,48 @@
|
|
|
247
247
|
[data-radius='pill'] .gram-elements {
|
|
248
248
|
--radius: 9999px;
|
|
249
249
|
}
|
|
250
|
+
|
|
251
|
+
/* assistant-ui loading dot styles (from @assistant-ui/react-markdown/styles/dot.css) */
|
|
252
|
+
@keyframes aui-pulse {
|
|
253
|
+
50% {
|
|
254
|
+
opacity: 0.5;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.gram-elements :where(.aui-md[data-status='running']):empty::after,
|
|
259
|
+
.gram-elements
|
|
260
|
+
:where(.aui-md[data-status='running'])
|
|
261
|
+
> :where(:not(ol):not(ul):not(pre)):last-child::after,
|
|
262
|
+
.gram-elements :where(.aui-md[data-status='running']) > pre:last-child code::after,
|
|
263
|
+
.gram-elements
|
|
264
|
+
:where(.aui-md[data-status='running'])
|
|
265
|
+
> :where(:is(ol, ul):last-child)
|
|
266
|
+
> :where(li:last-child:not(:has(* > li)))::after,
|
|
267
|
+
.gram-elements
|
|
268
|
+
:where(.aui-md[data-status='running'])
|
|
269
|
+
> :where(:is(ol, ul):last-child)
|
|
270
|
+
> :where(li:last-child)
|
|
271
|
+
> :where(:is(ol, ul):last-child)
|
|
272
|
+
> :where(li:last-child:not(:has(* > li)))::after,
|
|
273
|
+
.gram-elements
|
|
274
|
+
:where(.aui-md[data-status='running'])
|
|
275
|
+
> :where(:is(ol, ul):last-child)
|
|
276
|
+
> :where(li:last-child)
|
|
277
|
+
> :where(:is(ol, ul):last-child)
|
|
278
|
+
> :where(li:last-child)
|
|
279
|
+
> :where(:is(ol, ul):last-child)
|
|
280
|
+
> :where(li:last-child)::after {
|
|
281
|
+
animation: aui-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
282
|
+
font-family:
|
|
283
|
+
ui-sans-serif,
|
|
284
|
+
system-ui,
|
|
285
|
+
sans-serif,
|
|
286
|
+
'Apple Color Emoji',
|
|
287
|
+
'Segoe UI Emoji',
|
|
288
|
+
'Segoe UI Symbol',
|
|
289
|
+
'Noto Color Emoji';
|
|
290
|
+
--aui-content: '\25cf';
|
|
291
|
+
content: var(--aui-content);
|
|
292
|
+
margin-left: 0.25rem;
|
|
293
|
+
margin-right: 0.25rem;
|
|
294
|
+
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
useAssistantApi,
|
|
7
7
|
type AssistantApi,
|
|
8
8
|
} from '@assistant-ui/react'
|
|
9
|
-
import type
|
|
9
|
+
import { createAssistantStream, type AssistantStream } from 'assistant-stream'
|
|
10
10
|
import {
|
|
11
11
|
GramChatOverview,
|
|
12
12
|
GramChat,
|
|
@@ -21,9 +21,26 @@ import {
|
|
|
21
21
|
type PropsWithChildren,
|
|
22
22
|
} from 'react'
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Prefix used by assistant-ui for local thread IDs that haven't been persisted yet.
|
|
26
|
+
* This is an internal implementation detail of assistant-ui's RemoteThreadListThreadListRuntimeCore.
|
|
27
|
+
* If the library changes this prefix, we only need to update it here.
|
|
28
|
+
*/
|
|
29
|
+
const LOCAL_THREAD_ID_PREFIX = '__LOCALID_'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Checks if a thread ID is a local (unpersisted) thread ID.
|
|
33
|
+
* Local IDs are generated by assistant-ui before the thread is initialized with a remote ID.
|
|
34
|
+
*/
|
|
35
|
+
export function isLocalThreadId(threadId: string | undefined): boolean {
|
|
36
|
+
return !!threadId?.startsWith(LOCAL_THREAD_ID_PREFIX)
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
export interface ThreadListAdapterOptions {
|
|
25
40
|
apiUrl: string
|
|
26
41
|
headers: Record<string, string>
|
|
42
|
+
/** Map to translate local thread IDs to UUIDs (shared with transport) */
|
|
43
|
+
localIdToUuidMap?: Map<string, string>
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
interface ListChatsResponse {
|
|
@@ -227,6 +244,26 @@ export function useGramThreadListAdapter(
|
|
|
227
244
|
},
|
|
228
245
|
|
|
229
246
|
async initialize(threadId: string) {
|
|
247
|
+
// For new threads (local IDs), check if sendMessages already created a UUID
|
|
248
|
+
if (isLocalThreadId(threadId)) {
|
|
249
|
+
// Check if transport already generated a UUID for this local ID
|
|
250
|
+
const existingUuid =
|
|
251
|
+
optionsRef.current.localIdToUuidMap?.get(threadId)
|
|
252
|
+
if (existingUuid) {
|
|
253
|
+
return {
|
|
254
|
+
remoteId: existingUuid,
|
|
255
|
+
externalId: existingUuid,
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Otherwise generate a new one and store it
|
|
259
|
+
const uuid = crypto.randomUUID()
|
|
260
|
+
optionsRef.current.localIdToUuidMap?.set(threadId, uuid)
|
|
261
|
+
return {
|
|
262
|
+
remoteId: uuid,
|
|
263
|
+
externalId: uuid,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// For existing threads, use the ID as-is
|
|
230
267
|
return {
|
|
231
268
|
remoteId: threadId,
|
|
232
269
|
externalId: threadId,
|
|
@@ -250,21 +287,65 @@ export function useGramThreadListAdapter(
|
|
|
250
287
|
},
|
|
251
288
|
|
|
252
289
|
async generateTitle(
|
|
253
|
-
|
|
254
|
-
_remoteId: string,
|
|
290
|
+
remoteId: string,
|
|
255
291
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
256
292
|
_messages: readonly ThreadMessage[]
|
|
257
293
|
): Promise<AssistantStream> {
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
start(controller) {
|
|
294
|
+
// Skip if this is a local ID that hasn't been persisted yet
|
|
295
|
+
if (!remoteId || isLocalThreadId(remoteId)) {
|
|
296
|
+
return createAssistantStream((controller) => {
|
|
262
297
|
controller.close()
|
|
263
|
-
}
|
|
264
|
-
}
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Call the server to generate and persist the title
|
|
302
|
+
try {
|
|
303
|
+
const response = await fetch(
|
|
304
|
+
`${optionsRef.current.apiUrl}/rpc/chat.generateTitle`,
|
|
305
|
+
{
|
|
306
|
+
method: 'POST',
|
|
307
|
+
headers: {
|
|
308
|
+
...optionsRef.current.headers,
|
|
309
|
+
'Content-Type': 'application/json',
|
|
310
|
+
},
|
|
311
|
+
body: JSON.stringify({ id: remoteId }),
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if (response.ok) {
|
|
316
|
+
const result = (await response.json()) as { title: string }
|
|
317
|
+
const title = result.title || 'New Chat'
|
|
318
|
+
|
|
319
|
+
// Return a stream that emits the title as text
|
|
320
|
+
return createAssistantStream((controller) => {
|
|
321
|
+
controller.appendText(title)
|
|
322
|
+
controller.close()
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 404 is expected for new chats that haven't been persisted yet
|
|
327
|
+
if (response.status !== 404) {
|
|
328
|
+
console.error('Error generating title:', response.status)
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error('Error generating title:', error)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Fallback: return empty stream
|
|
335
|
+
return createAssistantStream((controller) => {
|
|
336
|
+
controller.close()
|
|
337
|
+
})
|
|
265
338
|
},
|
|
266
339
|
|
|
267
340
|
async fetch(threadId: string) {
|
|
341
|
+
// Skip if this is a local ID that hasn't been persisted yet
|
|
342
|
+
if (!threadId || isLocalThreadId(threadId)) {
|
|
343
|
+
return {
|
|
344
|
+
remoteId: threadId,
|
|
345
|
+
status: 'regular' as const,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
268
349
|
try {
|
|
269
350
|
const response = await fetch(
|
|
270
351
|
`${optionsRef.current.apiUrl}/rpc/chat.load?id=${encodeURIComponent(threadId)}`,
|
package/src/hooks/useMCPTools.ts
CHANGED
|
@@ -11,17 +11,19 @@ export function useMCPTools({
|
|
|
11
11
|
auth,
|
|
12
12
|
mcp,
|
|
13
13
|
environment,
|
|
14
|
+
gramEnvironment,
|
|
14
15
|
}: {
|
|
15
16
|
auth: Auth
|
|
16
17
|
mcp: string | undefined
|
|
17
18
|
environment: Record<string, unknown>
|
|
19
|
+
gramEnvironment?: string
|
|
18
20
|
}): UseQueryResult<MCPToolsResult, Error> {
|
|
19
21
|
const authQueryKey = Object.entries(auth.headers ?? {}).map(
|
|
20
22
|
(k, v) => `${k}:${v}`
|
|
21
23
|
)
|
|
22
24
|
|
|
23
25
|
const queryResult = useQuery({
|
|
24
|
-
queryKey: ['mcpTools', mcp, ...authQueryKey],
|
|
26
|
+
queryKey: ['mcpTools', mcp, gramEnvironment, ...authQueryKey],
|
|
25
27
|
queryFn: async () => {
|
|
26
28
|
assert(!auth.isLoading, 'No auth found')
|
|
27
29
|
assert(mcp, 'No MCP URL found')
|
|
@@ -34,6 +36,7 @@ export function useMCPTools({
|
|
|
34
36
|
headers: {
|
|
35
37
|
...transformEnvironmentToHeaders(environment ?? {}),
|
|
36
38
|
...auth.headers,
|
|
39
|
+
...(gramEnvironment && { 'Gram-Environment': gramEnvironment }),
|
|
37
40
|
},
|
|
38
41
|
},
|
|
39
42
|
})
|
|
@@ -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
|
@@ -9,7 +9,7 @@ export { useElements } from './hooks/useElements'
|
|
|
9
9
|
|
|
10
10
|
// Core Components
|
|
11
11
|
export { Chat } from '@/components/Chat'
|
|
12
|
-
export {
|
|
12
|
+
export { ChatHistory } from '@/components/ChatHistory'
|
|
13
13
|
|
|
14
14
|
// Frontend Tools
|
|
15
15
|
export { defineFrontendTool } from './lib/tools'
|
|
@@ -43,6 +43,7 @@ export type {
|
|
|
43
43
|
SidecarConfig,
|
|
44
44
|
Suggestion,
|
|
45
45
|
ThemeConfig,
|
|
46
|
+
ToolMentionsConfig,
|
|
46
47
|
ToolsConfig,
|
|
47
48
|
Variant,
|
|
48
49
|
VARIANTS,
|
|
@@ -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
|
@@ -111,6 +111,13 @@ export interface ElementsConfig {
|
|
|
111
111
|
*/
|
|
112
112
|
environment?: Record<string, unknown>
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The environment slug to use for resolving secrets.
|
|
116
|
+
* When specified, this is sent as the Gram-Environment header to select
|
|
117
|
+
* which environment's secrets to use for tool execution.
|
|
118
|
+
*/
|
|
119
|
+
gramEnvironment?: string
|
|
120
|
+
|
|
114
121
|
/**
|
|
115
122
|
* The layout variant for the chat interface.
|
|
116
123
|
*
|
|
@@ -747,6 +754,13 @@ export interface ComposerConfig {
|
|
|
747
754
|
* @default true
|
|
748
755
|
*/
|
|
749
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
|
|
750
764
|
}
|
|
751
765
|
|
|
752
766
|
/**
|
|
@@ -774,6 +788,14 @@ export interface AttachmentsConfig {
|
|
|
774
788
|
maxSize?: number
|
|
775
789
|
}
|
|
776
790
|
|
|
791
|
+
export interface ToolMentionsConfig {
|
|
792
|
+
/** @default true */
|
|
793
|
+
enabled?: boolean
|
|
794
|
+
/** @default 10 */
|
|
795
|
+
maxSuggestions?: number
|
|
796
|
+
placeholder?: string
|
|
797
|
+
}
|
|
798
|
+
|
|
777
799
|
export interface SidecarConfig extends ExpandableConfig {
|
|
778
800
|
/**
|
|
779
801
|
* The title displayed in the sidecar header.
|
|
@@ -822,4 +844,5 @@ export type ElementsContextType = {
|
|
|
822
844
|
isOpen: boolean
|
|
823
845
|
setIsOpen: (isOpen: boolean) => void
|
|
824
846
|
plugins: Plugin[]
|
|
847
|
+
mcpTools: Record<string, unknown> | undefined
|
|
825
848
|
}
|