@tanstack/ai-solid 0.19.4 → 0.21.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/create-chat-hook.d.ts +39 -0
- package/dist/create-chat-hook.js +38 -0
- package/dist/create-chat-hook.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/types.d.ts +4 -4
- package/dist/use-chat.d.ts +2 -2
- package/dist/use-web-mcp-tools.d.ts +26 -0
- package/dist/use-web-mcp-tools.js +31 -0
- package/dist/use-web-mcp-tools.js.map +1 -0
- package/package.json +16 -4
- package/src/chat-ui/chat-input.tsx +168 -0
- package/src/chat-ui/chat-message.tsx +261 -0
- package/src/chat-ui/chat-messages.tsx +91 -0
- package/src/chat-ui/chat.tsx +95 -0
- package/src/chat-ui/create-chat-hook.tsx +77 -0
- package/src/chat-ui/create-ui-contexts.tsx +52 -0
- package/src/chat-ui/create-ui.tsx +718 -0
- package/src/chat-ui/markdown-plugins.ts +48 -0
- package/src/chat-ui/text-part.tsx +71 -0
- package/src/chat-ui/thinking-part.tsx +79 -0
- package/src/chat-ui/tool-approval.tsx +121 -0
- package/src/create-chat-hook.ts +67 -0
- package/src/index.ts +3 -0
- package/src/ui.ts +39 -0
- package/src/use-web-mcp-tools.ts +55 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import remarkGfm from 'remark-gfm'
|
|
2
|
+
import rehypeRaw from 'rehype-raw'
|
|
3
|
+
import rehypeSanitize from 'rehype-sanitize'
|
|
4
|
+
import rehypeHighlight from 'rehype-highlight'
|
|
5
|
+
import type { SolidMarkdownOptions } from 'solid-markdown'
|
|
6
|
+
|
|
7
|
+
export type PluggableList = SolidMarkdownOptions['remarkPlugins']
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_REMARK_PLUGINS: PluggableList = [remarkGfm]
|
|
10
|
+
export const DEFAULT_REHYPE_PLUGINS_BEFORE_USER: PluggableList = [
|
|
11
|
+
rehypeRaw,
|
|
12
|
+
rehypeHighlight,
|
|
13
|
+
]
|
|
14
|
+
export const DEFAULT_REHYPE_PLUGINS_AFTER_USER: PluggableList = [rehypeSanitize]
|
|
15
|
+
|
|
16
|
+
export interface ResolveMarkdownPluginsOptions {
|
|
17
|
+
remarkPlugins?: PluggableList
|
|
18
|
+
rehypePlugins?: PluggableList
|
|
19
|
+
disableDefaultPlugins?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResolvedMarkdownPlugins {
|
|
23
|
+
remarkPlugins: PluggableList
|
|
24
|
+
rehypePlugins: PluggableList
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveMarkdownPlugins(
|
|
28
|
+
options: ResolveMarkdownPluginsOptions,
|
|
29
|
+
): ResolvedMarkdownPlugins {
|
|
30
|
+
const userRemark = options.remarkPlugins ?? []
|
|
31
|
+
const userRehype = options.rehypePlugins ?? []
|
|
32
|
+
|
|
33
|
+
if (options.disableDefaultPlugins) {
|
|
34
|
+
return {
|
|
35
|
+
remarkPlugins: [...userRemark],
|
|
36
|
+
rehypePlugins: [...userRehype],
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
remarkPlugins: [...DEFAULT_REMARK_PLUGINS, ...userRemark],
|
|
42
|
+
rehypePlugins: [
|
|
43
|
+
...DEFAULT_REHYPE_PLUGINS_BEFORE_USER,
|
|
44
|
+
...userRehype,
|
|
45
|
+
...DEFAULT_REHYPE_PLUGINS_AFTER_USER,
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { SolidMarkdown } from 'solid-markdown'
|
|
2
|
+
import { resolveMarkdownPlugins } from './markdown-plugins'
|
|
3
|
+
import type { SolidMarkdownComponents } from 'solid-markdown'
|
|
4
|
+
import type { PluggableList } from './markdown-plugins'
|
|
5
|
+
|
|
6
|
+
export interface TextPartProps {
|
|
7
|
+
/** The text content to render */
|
|
8
|
+
content: string
|
|
9
|
+
/** The role of the message (user, assistant, or system) - optional for standalone use */
|
|
10
|
+
role?: 'user' | 'assistant' | 'system'
|
|
11
|
+
/** Base class applied to all text parts */
|
|
12
|
+
class?: string
|
|
13
|
+
/** Additional class for user messages */
|
|
14
|
+
userClass?: string
|
|
15
|
+
/** Additional class for assistant messages (also used for system messages) */
|
|
16
|
+
assistantClass?: string
|
|
17
|
+
/** Additional remark plugins, appended after the defaults. */
|
|
18
|
+
remarkPlugins?: PluggableList
|
|
19
|
+
/**
|
|
20
|
+
* Additional rehype plugins. Inserted before the trailing
|
|
21
|
+
* `rehypeSanitize` so sanitization always runs last.
|
|
22
|
+
*/
|
|
23
|
+
rehypePlugins?: PluggableList
|
|
24
|
+
/** solid-markdown `components` overrides. */
|
|
25
|
+
components?: SolidMarkdownComponents
|
|
26
|
+
/**
|
|
27
|
+
* Drop the built-in plugin defaults entirely. Disables the XSS
|
|
28
|
+
* sanitizer; the caller becomes responsible for sanitization.
|
|
29
|
+
*/
|
|
30
|
+
disableDefaultPlugins?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* TextPart component - renders markdown text with syntax highlighting.
|
|
35
|
+
*
|
|
36
|
+
* @example Add a markdown plugin (e.g. CJK bold/emphasis support)
|
|
37
|
+
* ```tsx
|
|
38
|
+
* import remarkCjkFriendly from 'remark-cjk-friendly'
|
|
39
|
+
*
|
|
40
|
+
* <TextPart content={content} remarkPlugins={[remarkCjkFriendly]} />
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function TextPart(props: TextPartProps) {
|
|
44
|
+
const roleClass = () =>
|
|
45
|
+
props.role === 'user'
|
|
46
|
+
? (props.userClass ?? '')
|
|
47
|
+
: props.role === 'assistant'
|
|
48
|
+
? (props.assistantClass ?? '')
|
|
49
|
+
: ''
|
|
50
|
+
const combinedClass = () =>
|
|
51
|
+
[props.class ?? '', roleClass()].filter(Boolean).join(' ')
|
|
52
|
+
|
|
53
|
+
const resolved = () =>
|
|
54
|
+
resolveMarkdownPlugins({
|
|
55
|
+
remarkPlugins: props.remarkPlugins,
|
|
56
|
+
rehypePlugins: props.rehypePlugins,
|
|
57
|
+
disableDefaultPlugins: props.disableDefaultPlugins,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div class={combinedClass() || undefined}>
|
|
62
|
+
<SolidMarkdown
|
|
63
|
+
remarkPlugins={resolved().remarkPlugins}
|
|
64
|
+
rehypePlugins={resolved().rehypePlugins}
|
|
65
|
+
components={props.components}
|
|
66
|
+
>
|
|
67
|
+
{props.content}
|
|
68
|
+
</SolidMarkdown>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Show, createEffect, createSignal } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
export interface ThinkingPartProps {
|
|
4
|
+
/** The thinking content to render */
|
|
5
|
+
content: string
|
|
6
|
+
/** Base class applied to thinking parts */
|
|
7
|
+
class?: string
|
|
8
|
+
/** Whether thinking is complete (has text content after) */
|
|
9
|
+
isComplete?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ThinkingPart component - renders thinking/reasoning content
|
|
14
|
+
*
|
|
15
|
+
* This component displays the model's internal reasoning process,
|
|
16
|
+
* typically shown in a collapsed or expandable format to distinguish
|
|
17
|
+
* it from the final response. It automatically collapses when thinking
|
|
18
|
+
* is complete.
|
|
19
|
+
*
|
|
20
|
+
* @example Standalone usage
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <ThinkingPart
|
|
23
|
+
* content="Let me think about this step by step..."
|
|
24
|
+
* class="p-4 rounded bg-gray-100"
|
|
25
|
+
* />
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example Usage in partRenderers
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <ChatMessage
|
|
31
|
+
* message={message}
|
|
32
|
+
* partRenderers={{
|
|
33
|
+
* thinking: ({ content, isComplete }) => (
|
|
34
|
+
* <ThinkingPart
|
|
35
|
+
* content={content}
|
|
36
|
+
* isComplete={isComplete}
|
|
37
|
+
* class="px-5 py-3 rounded-2xl bg-gray-800/50"
|
|
38
|
+
* />
|
|
39
|
+
* )
|
|
40
|
+
* }}
|
|
41
|
+
* />
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function ThinkingPart(props: ThinkingPartProps) {
|
|
45
|
+
const [isCollapsed, setIsCollapsed] = createSignal(false)
|
|
46
|
+
|
|
47
|
+
// Auto-collapse when thinking completes
|
|
48
|
+
createEffect(() => {
|
|
49
|
+
if (props.isComplete) {
|
|
50
|
+
setIsCollapsed(true)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
class={props.class || undefined}
|
|
57
|
+
data-part-type="thinking"
|
|
58
|
+
data-part-content
|
|
59
|
+
>
|
|
60
|
+
<button
|
|
61
|
+
onClick={() => setIsCollapsed(!isCollapsed())}
|
|
62
|
+
class="flex items-center gap-2 text-sm text-gray-400 hover:text-gray-300 transition-colors mb-2"
|
|
63
|
+
aria-expanded={!isCollapsed()}
|
|
64
|
+
aria-label={isCollapsed() ? 'Expand thinking' : 'Collapse thinking'}
|
|
65
|
+
>
|
|
66
|
+
<span class="text-xs">{isCollapsed() ? '▶' : '▼'}</span>
|
|
67
|
+
<span class="italic">💭 Thinking...</span>
|
|
68
|
+
<Show when={props.isComplete}>
|
|
69
|
+
<span class="text-xs text-gray-500">(complete)</span>
|
|
70
|
+
</Show>
|
|
71
|
+
</button>
|
|
72
|
+
<Show when={!isCollapsed()}>
|
|
73
|
+
<div class="text-gray-300 whitespace-pre-wrap font-mono text-sm">
|
|
74
|
+
{props.content}
|
|
75
|
+
</div>
|
|
76
|
+
</Show>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useChatContext } from './chat'
|
|
2
|
+
import type { JSX } from 'solid-js'
|
|
3
|
+
|
|
4
|
+
/** @deprecated Use `createChatUI()` interrupt components with `chat.interrupts`. Deprecated in 0.8.0. Removed in 1.0.0. */
|
|
5
|
+
export interface ToolApprovalProps {
|
|
6
|
+
/** Tool call ID */
|
|
7
|
+
toolCallId: string
|
|
8
|
+
/** Tool name */
|
|
9
|
+
toolName: string
|
|
10
|
+
/** Parsed tool arguments/input */
|
|
11
|
+
input: any
|
|
12
|
+
/** Approval metadata */
|
|
13
|
+
approval: {
|
|
14
|
+
id: string
|
|
15
|
+
needsApproval: boolean
|
|
16
|
+
approved?: boolean
|
|
17
|
+
}
|
|
18
|
+
/** CSS class name */
|
|
19
|
+
class?: string
|
|
20
|
+
/** Custom render prop */
|
|
21
|
+
children?: (props: ToolApprovalRenderProps) => JSX.Element
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ToolApprovalRenderProps {
|
|
25
|
+
/** Tool name */
|
|
26
|
+
toolName: string
|
|
27
|
+
/** Parsed input */
|
|
28
|
+
input: any
|
|
29
|
+
/** Approve the tool call */
|
|
30
|
+
onApprove: () => void
|
|
31
|
+
/** Deny the tool call */
|
|
32
|
+
onDeny: () => void
|
|
33
|
+
/** Whether user has responded */
|
|
34
|
+
hasResponded: boolean
|
|
35
|
+
/** User's decision (if responded) */
|
|
36
|
+
approved?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @deprecated Use `createChatUI()` interrupt components with `chat.interrupts`. Deprecated in 0.8.0. Removed in 1.0.0.
|
|
41
|
+
*
|
|
42
|
+
* Tool approval component - renders approve/deny buttons for tools that need approval
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* {part.approval && (
|
|
47
|
+
* <ToolApproval
|
|
48
|
+
* toolCallId={part.id}
|
|
49
|
+
* toolName={part.name}
|
|
50
|
+
* input={JSON.parse(part.arguments)}
|
|
51
|
+
* approval={part.approval}
|
|
52
|
+
* />
|
|
53
|
+
* )}
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function ToolApproval(props: ToolApprovalProps) {
|
|
57
|
+
const { addToolApprovalResponse } = useChatContext()
|
|
58
|
+
|
|
59
|
+
const handleApprove = () => {
|
|
60
|
+
void addToolApprovalResponse({
|
|
61
|
+
id: props.approval.id,
|
|
62
|
+
approved: true,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const handleDeny = () => {
|
|
67
|
+
void addToolApprovalResponse({
|
|
68
|
+
id: props.approval.id,
|
|
69
|
+
approved: false,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hasResponded = () => props.approval.approved !== undefined
|
|
74
|
+
|
|
75
|
+
const renderProps = (): ToolApprovalRenderProps => ({
|
|
76
|
+
toolName: props.toolName,
|
|
77
|
+
input: props.input,
|
|
78
|
+
onApprove: handleApprove,
|
|
79
|
+
onDeny: handleDeny,
|
|
80
|
+
hasResponded: hasResponded(),
|
|
81
|
+
approved: props.approval.approved,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Render prop pattern
|
|
85
|
+
if (props.children) {
|
|
86
|
+
return <>{props.children(renderProps())}</>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Already responded - show decision
|
|
90
|
+
if (hasResponded()) {
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
class={props.class}
|
|
94
|
+
data-tool-approval
|
|
95
|
+
data-approval-status={props.approval.approved ? 'approved' : 'denied'}
|
|
96
|
+
>
|
|
97
|
+
{props.approval.approved ? '✓ Approved' : '✗ Denied'}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Default approval UI
|
|
103
|
+
return (
|
|
104
|
+
<div class={props.class} data-tool-approval data-approval-status="pending">
|
|
105
|
+
<div data-approval-header>
|
|
106
|
+
<strong>{props.toolName}</strong> requires approval
|
|
107
|
+
</div>
|
|
108
|
+
<div data-approval-input>
|
|
109
|
+
<pre>{JSON.stringify(props.input, null, 2)}</pre>
|
|
110
|
+
</div>
|
|
111
|
+
<div data-approval-actions>
|
|
112
|
+
<button onClick={handleApprove} data-approval-approve>
|
|
113
|
+
Approve
|
|
114
|
+
</button>
|
|
115
|
+
<button onClick={handleDeny} data-approval-deny>
|
|
116
|
+
Deny
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useChat as useUnboundChat } from './use-chat'
|
|
2
|
+
import type {
|
|
3
|
+
AnyClientTool,
|
|
4
|
+
InterruptDefinition,
|
|
5
|
+
SchemaInput,
|
|
6
|
+
} from '@tanstack/ai'
|
|
7
|
+
import type { InferredClientContext } from '@tanstack/ai-client'
|
|
8
|
+
import type { UseChatOptions } from './types'
|
|
9
|
+
|
|
10
|
+
type ChatHookOverrides<
|
|
11
|
+
TTools extends ReadonlyArray<AnyClientTool>,
|
|
12
|
+
TSchema extends SchemaInput | undefined,
|
|
13
|
+
TContext,
|
|
14
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
15
|
+
> = {
|
|
16
|
+
threadId?: string
|
|
17
|
+
live?: boolean
|
|
18
|
+
forwardedProps?: Record<string, any>
|
|
19
|
+
body?: Record<string, any>
|
|
20
|
+
initialMessages?: UseChatOptions<
|
|
21
|
+
TTools,
|
|
22
|
+
TSchema,
|
|
23
|
+
TContext,
|
|
24
|
+
TInterrupts
|
|
25
|
+
>['initialMessages']
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Bind chat options once at module scope. The returned `useChat` primitive
|
|
30
|
+
* creates a chat instance from those options.
|
|
31
|
+
*
|
|
32
|
+
* Pass per-call overrides for instance keys such as `threadId`,
|
|
33
|
+
* `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
|
|
34
|
+
* `interrupts`, or `outputSchema` here. Those stay on the factory options.
|
|
35
|
+
*
|
|
36
|
+
* Rename the primitive at the call site if you already import `useChat` from
|
|
37
|
+
* `@tanstack/ai-solid`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const { useChat } = createChatHook(chatOptions)
|
|
42
|
+
*
|
|
43
|
+
* function Support() {
|
|
44
|
+
* const chat = useChat({ threadId: 'support-1' })
|
|
45
|
+
* return <UI.Chat chat={chat} components={components} />
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function createChatHook<
|
|
50
|
+
const TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
51
|
+
TSchema extends SchemaInput | undefined = undefined,
|
|
52
|
+
TContext = InferredClientContext<TTools>,
|
|
53
|
+
const TInterrupts extends ReadonlyArray<
|
|
54
|
+
InterruptDefinition<any, any, any, any>
|
|
55
|
+
> = readonly [],
|
|
56
|
+
>(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>) {
|
|
57
|
+
function useChat(
|
|
58
|
+
overrides?: ChatHookOverrides<TTools, TSchema, TContext, TInterrupts>,
|
|
59
|
+
) {
|
|
60
|
+
if (!overrides) {
|
|
61
|
+
return useUnboundChat(options)
|
|
62
|
+
}
|
|
63
|
+
return useUnboundChat({ ...options, ...overrides })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { useChat }
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { useChat } from './use-chat'
|
|
2
|
+
export { createChatHook } from './create-chat-hook'
|
|
2
3
|
export { useByok } from './use-byok'
|
|
4
|
+
export { useWebMCPTools } from './use-web-mcp-tools'
|
|
5
|
+
export type { UseWebMCPToolsOptions } from './use-web-mcp-tools'
|
|
3
6
|
export type {
|
|
4
7
|
DeepPartial,
|
|
5
8
|
UseChatOptions,
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Barrel entry for the `@tanstack/ai-solid/ui` subpath.
|
|
2
|
+
export {
|
|
3
|
+
createChatUI,
|
|
4
|
+
type ChatUIComponents,
|
|
5
|
+
type ChatUIFactoryConfig,
|
|
6
|
+
type ChatUIHost,
|
|
7
|
+
type ChatUIQueueItem,
|
|
8
|
+
type InputProps,
|
|
9
|
+
type InterruptProps,
|
|
10
|
+
type LayoutProps,
|
|
11
|
+
type MessageProps,
|
|
12
|
+
type PartProps,
|
|
13
|
+
type QueueProps,
|
|
14
|
+
type ToolProps,
|
|
15
|
+
} from './chat-ui/create-ui'
|
|
16
|
+
export { createChatHook } from './chat-ui/create-chat-hook'
|
|
17
|
+
export {
|
|
18
|
+
createChatHookContexts,
|
|
19
|
+
type ChatUIContexts,
|
|
20
|
+
} from './chat-ui/create-ui-contexts'
|
|
21
|
+
export { Chat, useChatContext, type ChatProps } from './chat-ui/chat'
|
|
22
|
+
export { ChatMessages, type ChatMessagesProps } from './chat-ui/chat-messages'
|
|
23
|
+
export {
|
|
24
|
+
ChatMessage,
|
|
25
|
+
type ChatMessageProps,
|
|
26
|
+
type ToolCallRenderProps,
|
|
27
|
+
} from './chat-ui/chat-message'
|
|
28
|
+
export {
|
|
29
|
+
ChatInput,
|
|
30
|
+
type ChatInputProps,
|
|
31
|
+
type ChatInputRenderProps,
|
|
32
|
+
} from './chat-ui/chat-input'
|
|
33
|
+
export {
|
|
34
|
+
ToolApproval,
|
|
35
|
+
type ToolApprovalProps,
|
|
36
|
+
type ToolApprovalRenderProps,
|
|
37
|
+
} from './chat-ui/tool-approval'
|
|
38
|
+
export { TextPart, type TextPartProps } from './chat-ui/text-part'
|
|
39
|
+
export { ThinkingPart, type ThinkingPartProps } from './chat-ui/thinking-part'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { onCleanup } from 'solid-js'
|
|
2
|
+
import { registerWebMCPTools } from '@tanstack/ai-client'
|
|
3
|
+
import type {
|
|
4
|
+
AnyClientTool,
|
|
5
|
+
InferredClientContext,
|
|
6
|
+
RegisterWebMCPToolsOptions,
|
|
7
|
+
} from '@tanstack/ai-client'
|
|
8
|
+
|
|
9
|
+
/** Options for {@link useWebMCPTools}. */
|
|
10
|
+
export type UseWebMCPToolsOptions<
|
|
11
|
+
TTools extends ReadonlyArray<AnyClientTool>,
|
|
12
|
+
TContext = InferredClientContext<TTools>,
|
|
13
|
+
> = Omit<RegisterWebMCPToolsOptions<TTools, TContext>, 'signal'> & {
|
|
14
|
+
/** Receives an asynchronous registration error. */
|
|
15
|
+
onError?: (error: unknown) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type UseWebMCPToolsArguments<
|
|
19
|
+
TTools extends ReadonlyArray<AnyClientTool>,
|
|
20
|
+
TContext,
|
|
21
|
+
> =
|
|
22
|
+
{} extends UseWebMCPToolsOptions<TTools, TContext>
|
|
23
|
+
? [options?: UseWebMCPToolsOptions<TTools, TContext>]
|
|
24
|
+
: [options: UseWebMCPToolsOptions<TTools, TContext>]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Registers executable client tools with WebMCP for the current Solid owner.
|
|
28
|
+
*
|
|
29
|
+
* The owner cleanup removes every tool registered by this call.
|
|
30
|
+
*
|
|
31
|
+
* @param tools - The executable client tools to expose through WebMCP.
|
|
32
|
+
* @param options - Runtime context, per-tool options, and error handling.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* useWebMCPTools([searchProducts])
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function useWebMCPTools<
|
|
40
|
+
const TTools extends ReadonlyArray<AnyClientTool>,
|
|
41
|
+
TContext = InferredClientContext<TTools>,
|
|
42
|
+
>(tools: TTools, ...[options]: UseWebMCPToolsArguments<TTools, TContext>) {
|
|
43
|
+
const controller = new AbortController()
|
|
44
|
+
|
|
45
|
+
void registerWebMCPTools(tools, {
|
|
46
|
+
...options,
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
}).catch((error) => {
|
|
49
|
+
if (!controller.signal.aborted) {
|
|
50
|
+
options?.onError?.(error)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
onCleanup(() => controller.abort())
|
|
55
|
+
}
|