(null)
+
+ const disabled = disabledProp || isLoading
+
+ const handleSubmit = () => {
+ if (!value.trim() || disabled) return
+ sendMessage(value)
+ setValue('')
+ }
+
+ const renderProps: ChatInputRenderProps = {
+ value,
+ onChange: setValue,
+ onSubmit: handleSubmit,
+ isLoading,
+ disabled,
+ inputRef,
+ }
+
+ // Render prop pattern
+ if (children) {
+ return <>{children(renderProps)}>
+ }
+
+ // Default implementation
+ return (
+
+ }
+ type="text"
+ value={value}
+ onInput={(e) => setValue(e.currentTarget.value)}
+ onKeyDown={(e) => {
+ if (submitOnEnter && e.key === 'Enter') {
+ e.preventDefault()
+ handleSubmit()
+ }
+ }}
+ placeholder={placeholder}
+ disabled={disabled}
+ data-chat-textarea
+ style={{
+ flex: 1,
+ padding: '0.75rem 1rem',
+ fontSize: '0.875rem',
+ border: '1px solid rgba(255, 255, 255, 0.1)',
+ borderRadius: '0.75rem',
+ backgroundColor: 'rgba(31, 41, 55, 0.5)',
+ color: 'white',
+ outline: 'none',
+ transition: 'all 0.2s',
+ }}
+ onFocus={(e) => {
+ e.currentTarget.style.borderColor = 'rgba(249, 115, 22, 0.4)'
+ e.currentTarget.style.boxShadow = '0 0 0 2px rgba(249, 115, 22, 0.2)'
+ }}
+ onBlur={(e) => {
+ e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.1)'
+ e.currentTarget.style.boxShadow = 'none'
+ }}
+ />
+
+
+ )
+}
diff --git a/packages/typescript/ai-preact-ui/src/chat-message.tsx b/packages/typescript/ai-preact-ui/src/chat-message.tsx
new file mode 100644
index 00000000..bf17b4c9
--- /dev/null
+++ b/packages/typescript/ai-preact-ui/src/chat-message.tsx
@@ -0,0 +1,269 @@
+import { ThinkingPart } from './thinking-part'
+import type { ComponentChildren } from 'preact'
+import type { UIMessage } from '@tanstack/ai-preact'
+
+export interface ToolCallRenderProps {
+ id: string
+ name: string
+ arguments: string
+ state: string
+ approval?: any
+ output?: any
+}
+
+export interface ChatMessageProps {
+ /** The message to render */
+ message: UIMessage
+ /** Base CSS class name */
+ className?: string
+ /** Additional className for user messages */
+ userClassName?: string
+ /** Additional className for assistant messages */
+ assistantClassName?: string
+ /** Custom renderer for text parts */
+ textPartRenderer?: (props: { content: string }) => ComponentChildren
+ /** Custom renderer for thinking parts */
+ thinkingPartRenderer?: (props: {
+ content: string
+ isComplete?: boolean
+ }) => ComponentChildren
+ /** Named tool renderers - use the tool name as the key */
+ toolsRenderer?: Record<
+ string,
+ (props: ToolCallRenderProps) => ComponentChildren
+ >
+ /** Default tool renderer when tool name not found in toolsRenderer */
+ defaultToolRenderer?: (props: ToolCallRenderProps) => ComponentChildren
+ /** Custom renderer for tool result parts */
+ toolResultRenderer?: (props: {
+ toolCallId: string
+ content: string
+ state: string
+ }) => ComponentChildren
+}
+
+/**
+ * Message component - renders a single message with all its parts
+ *
+ * This component natively understands TanStack AI's parts-based message format:
+ * - thinking parts: rendered as collapsible thinking/reasoning sections (auto-collapses when complete)
+ * - text parts: rendered as content
+ * - tool-call parts: rendered with state, approvals, etc.
+ * - tool-result parts: rendered with results
+ *
+ * @example Basic usage
+ * ```tsx
+ *
+ * ```
+ *
+ * @example With role-based styling
+ * ```tsx
+ *
+ * ```
+ *
+ * @example With custom thinking renderer
+ * ```tsx
+ * (
+ *
+ * 💭 Thinking...
+ * {content}
+ *
+ * )}
+ * />
+ * ```
+ *
+ * @example With named tool renderers
+ * ```tsx
+ * ,
+ * weatherLookup: ({ id, arguments: args }) => ,
+ * }}
+ * defaultToolRenderer={() => null}
+ * />
+ * ```
+ */
+export function ChatMessage({
+ message,
+ className = '',
+ userClassName = '',
+ assistantClassName = '',
+ textPartRenderer,
+ thinkingPartRenderer,
+ toolsRenderer,
+ defaultToolRenderer,
+ toolResultRenderer,
+}: ChatMessageProps) {
+ // Combine classes based on role
+ const roleClassName =
+ message.role === 'user' ? userClassName : assistantClassName
+
+ const combinedClassName = [className, roleClassName].filter(Boolean).join(' ')
+
+ return (
+
+ {message.parts.map((part, index) => {
+ // Check if thinking is complete (if there's a text part after this thinking part)
+ const isThinkingComplete =
+ part.type === 'thinking' &&
+ message.parts.slice(index + 1).some((p) => p.type === 'text')
+
+ return (
+
+ )
+ })}
+
+ )
+}
+
+function MessagePart({
+ part,
+ isThinkingComplete,
+ textPartRenderer,
+ thinkingPartRenderer,
+ toolsRenderer,
+ defaultToolRenderer,
+ toolResultRenderer,
+}: {
+ part: any
+ isThinkingComplete?: boolean
+ textPartRenderer?: ChatMessageProps['textPartRenderer']
+ thinkingPartRenderer?: ChatMessageProps['thinkingPartRenderer']
+ toolsRenderer?: ChatMessageProps['toolsRenderer']
+ defaultToolRenderer?: ChatMessageProps['defaultToolRenderer']
+ toolResultRenderer?: ChatMessageProps['toolResultRenderer']
+}) {
+ // Text part
+ if (part.type === 'text') {
+ if (textPartRenderer) {
+ return <>{textPartRenderer({ content: part.content })}>
+ }
+ return (
+
+ {part.content}
+
+ )
+ }
+
+ // Thinking part
+ if (part.type === 'thinking') {
+ if (thinkingPartRenderer) {
+ return (
+ <>
+ {thinkingPartRenderer({
+ content: part.content,
+ isComplete: isThinkingComplete,
+ })}
+ >
+ )
+ }
+ return (
+
+ )
+ }
+
+ // Tool call part
+ if (part.type === 'tool-call') {
+ const toolProps: ToolCallRenderProps = {
+ id: part.id,
+ name: part.name,
+ arguments: part.arguments,
+ state: part.state,
+ approval: part.approval,
+ output: part.output,
+ }
+
+ // Check if there's a specific renderer for this tool
+ if (toolsRenderer?.[part.name]) {
+ return <>{toolsRenderer[part.name]?.(toolProps)}>
+ }
+
+ // Use default tool renderer if provided
+ if (defaultToolRenderer) {
+ return <>{defaultToolRenderer(toolProps)}>
+ }
+
+ // Fallback to built-in default renderer
+ return (
+
+
+ {part.name}
+ {part.state}
+
+ {part.arguments && (
+
+ )}
+ {part.approval && (
+
+ {part.approval.approved !== undefined
+ ? part.approval.approved
+ ? '✓ Approved'
+ : '✗ Denied'
+ : '⏳ Awaiting approval...'}
+
+ )}
+ {part.output && (
+
+
{JSON.stringify(part.output, null, 2)}
+
+ )}
+
+ )
+ }
+
+ // Tool result part
+ if (part.type === 'tool-result') {
+ if (toolResultRenderer) {
+ return (
+ <>
+ {toolResultRenderer({
+ toolCallId: part.toolCallId,
+ content: part.content,
+ state: part.state,
+ })}
+ >
+ )
+ }
+
+ return (
+
+ )
+ }
+
+ return null
+}
diff --git a/packages/typescript/ai-preact-ui/src/chat-messages.tsx b/packages/typescript/ai-preact-ui/src/chat-messages.tsx
new file mode 100644
index 00000000..30b62c6b
--- /dev/null
+++ b/packages/typescript/ai-preact-ui/src/chat-messages.tsx
@@ -0,0 +1,86 @@
+import { useEffect, useRef } from 'preact/hooks'
+import { useChatContext } from './chat'
+import { ChatMessage } from './chat-message'
+import type { ComponentChildren } from 'preact'
+import type { UIMessage } from '@tanstack/ai-preact'
+
+export interface ChatMessagesProps {
+ /** Custom render function for each message */
+ children?: (message: UIMessage, index: number) => ComponentChildren
+ /** CSS class name */
+ className?: string
+ /** Element to show when there are no messages */
+ emptyState?: ComponentChildren
+ /** Element to show while loading the first message */
+ loadingState?: ComponentChildren
+ /** Custom error renderer */
+ errorState?: (props: {
+ error: Error
+ reload: () => void
+ }) => ComponentChildren
+ /** Auto-scroll to bottom on new messages */
+ autoScroll?: boolean
+}
+
+/**
+ * Messages container - renders all messages in the conversation
+ *
+ * @example
+ * ```tsx
+ *
+ * {(message) => }
+ *
+ * ```
+ */
+export function ChatMessages({
+ children,
+ className,
+ emptyState,
+ loadingState,
+ errorState,
+ autoScroll = true,
+}: ChatMessagesProps) {
+ const { messages, isLoading, error, reload } = useChatContext()
+ const containerRef = useRef(null)
+
+ // Auto-scroll to bottom on new messages
+ useEffect(() => {
+ if (autoScroll && containerRef.current) {
+ containerRef.current.scrollTop = containerRef.current.scrollHeight
+ }
+ }, [messages, autoScroll])
+
+ // Error state
+ if (error && errorState) {
+ return <>{errorState({ error, reload })}>
+ }
+
+ // Loading state (only show if no messages yet)
+ if (isLoading && messages.length === 0 && loadingState) {
+ return <>{loadingState}>
+ }
+
+ // Empty state
+ if (messages.length === 0 && emptyState) {
+ return <>{emptyState}>
+ }
+
+ return (
+
+ {messages.map((message, index) =>
+ children ? (
+
+ {children(message, index)}
+
+ ) : (
+
+ ),
+ )}
+
+ )
+}
diff --git a/packages/typescript/ai-preact-ui/src/chat.tsx b/packages/typescript/ai-preact-ui/src/chat.tsx
new file mode 100644
index 00000000..2b76ff1c
--- /dev/null
+++ b/packages/typescript/ai-preact-ui/src/chat.tsx
@@ -0,0 +1,96 @@
+import { createContext } from 'preact'
+import { useContext } from 'preact/hooks'
+import { useChat } from '@tanstack/ai-preact'
+import type { ComponentChildren } from 'preact'
+import type {
+ ConnectionAdapter,
+ UIMessage,
+ UseChatReturn,
+} from '@tanstack/ai-preact'
+
+/**
+ * Chat context - provides chat state to all child components
+ */
+const ChatContext = createContext(null)
+
+/**
+ * Hook to access chat context
+ * @throws Error if used outside of Chat component
+ */
+export function useChatContext() {
+ const context = useContext(ChatContext)
+ if (!context) {
+ throw new Error(
+ "Chat components must be wrapped in . Make sure you're using Chat.Messages, Chat.Input, etc. inside a component.",
+ )
+ }
+ return context
+}
+
+export interface ChatProps {
+ /** Child components (Chat.Messages, Chat.Input, etc.) */
+ children: ComponentChildren
+ /** CSS class name for the root element */
+ className?: string
+ /** Connection adapter for communicating with your API */
+ connection: ConnectionAdapter
+ /** Initial messages to display */
+ initialMessages?: Array
+ /** Custom message ID generator */
+ id?: string
+ /** Additional body data to send with requests */
+ body?: any
+ /** Callback when a response is received */
+ onResponse?: (response?: Response) => void | Promise
+ /** Callback when each chunk arrives */
+ onChunk?: (chunk: any) => void
+ /** Callback when a message is complete */
+ onFinish?: (message: UIMessage) => void
+ /** Callback when an error occurs */
+ onError?: (error: Error) => void
+ /** Custom tool components registry */
+ tools?: Record
+}
+
+/**
+ * Root Chat component - provides context for all chat subcomponents
+ *
+ * @example
+ * ```tsx
+ *
+ *
+ *
+ *
+ * ```
+ */
+export function Chat({
+ children,
+ className,
+ connection,
+ initialMessages,
+ id,
+ body,
+ onResponse,
+ onChunk,
+ onFinish,
+ onError,
+}: ChatProps) {
+ const chat = useChat({
+ connection,
+ initialMessages,
+ id,
+ body,
+ onResponse,
+ onChunk,
+ onFinish,
+ onError,
+ })
+
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/packages/typescript/ai-preact-ui/src/index.ts b/packages/typescript/ai-preact-ui/src/index.ts
new file mode 100644
index 00000000..c54f175e
--- /dev/null
+++ b/packages/typescript/ai-preact-ui/src/index.ts
@@ -0,0 +1,61 @@
+/**
+ * @tanstack/ai-preact-ui
+ *
+ * Headless Preact components for building AI chat interfaces.
+ *
+ * Features:
+ * - Parts-based message rendering (text, tool calls, tool results)
+ * - Native tool approval workflows
+ * - Client-side tool execution support
+ * - Streaming support
+ * - Fully customizable with render props
+ * - Compound component pattern
+ *
+ * @example
+ * ```tsx
+ * import { Chat, ChatMessages, ChatInput, ChatMessage } from '@tanstack/ai-preact-ui'
+ *
+ *
+ *
+ * {(message) => }
+ *
+ *
+ *
+ * ```
+ */
+
+// Main components
+export { Chat, useChatContext, type ChatProps } from './chat'
+export { ChatMessages, type ChatMessagesProps } from './chat-messages'
+export {
+ ChatMessage,
+ type ChatMessageProps,
+ type ToolCallRenderProps,
+} from './chat-message'
+export {
+ ChatInput,
+ type ChatInputProps,
+ type ChatInputRenderProps,
+} from './chat-input'
+export {
+ ToolApproval,
+ type ToolApprovalProps,
+ type ToolApprovalRenderProps,
+} from './tool-approval'
+export { TextPart, type TextPartProps } from './text-part'
+export { ThinkingPart, type ThinkingPartProps } from './thinking-part'
+
+// Re-export hooks from @tanstack/ai-preact for convenience
+export { useChat } from '@tanstack/ai-preact'
+
+// Re-export types from @tanstack/ai-preact
+export type {
+ UIMessage,
+ MessagePart,
+ ToolCallPart,
+ ToolResultPart,
+ TextPart as TextPartType,
+ ConnectionAdapter,
+} from '@tanstack/ai-client'
+
+export type { UseChatOptions, UseChatReturn } from '@tanstack/ai-preact'
diff --git a/packages/typescript/ai-preact-ui/src/text-part.tsx b/packages/typescript/ai-preact-ui/src/text-part.tsx
new file mode 100644
index 00000000..6f61a3d7
--- /dev/null
+++ b/packages/typescript/ai-preact-ui/src/text-part.tsx
@@ -0,0 +1,83 @@
+import { Markdown } from 'preact-md'
+import rehypeRaw from 'rehype-raw'
+import rehypeHighlight from 'rehype-highlight'
+import remarkGfm from 'remark-gfm'
+
+export interface TextPartProps {
+ /** The text content to render */
+ content: string
+ /** The role of the message (user, assistant, or system) - optional for standalone use */
+ role?: 'user' | 'assistant' | 'system'
+ /** Base className applied to all text parts */
+ className?: string
+ /** Additional className for user messages */
+ userClassName?: string
+ /** Additional className for assistant messages (also used for system messages) */
+ assistantClassName?: string
+}
+
+/**
+ * TextPart component - renders markdown text with syntax highlighting
+ *
+ * Features:
+ * - Full markdown support with GFM (tables, strikethrough, etc.)
+ * - Syntax highlighting for code blocks
+ * - Sanitized HTML rendering
+ * - Role-based styling (user vs assistant)
+ *
+ * @example Standalone usage
+ * ```tsx
+ *
+ * ```
+ *
+ * @example Usage in partRenderers
+ * ```tsx
+ * (
+ *
+ * )
+ * }}
+ * />
+ * ```
+ */
+export function TextPart({
+ content,
+ role,
+ className = '',
+ userClassName = '',
+ assistantClassName = '',
+}: TextPartProps) {
+ // Combine classes based on role
+ const roleClassName =
+ role === 'user'
+ ? userClassName
+ : role === 'assistant'
+ ? assistantClassName
+ : ''
+ const combinedClassName = [className, roleClassName].filter(Boolean).join(' ')
+
+ return (
+
+
+ {content}
+
+
+ )
+}
diff --git a/packages/typescript/ai-preact-ui/src/thinking-part.tsx b/packages/typescript/ai-preact-ui/src/thinking-part.tsx
new file mode 100644
index 00000000..9e841bb6
--- /dev/null
+++ b/packages/typescript/ai-preact-ui/src/thinking-part.tsx
@@ -0,0 +1,83 @@
+import { useEffect, useState } from 'preact/hooks'
+
+export interface ThinkingPartProps {
+ /** The thinking content to render */
+ content: string
+ /** Base className applied to thinking parts */
+ className?: string
+ /** Whether thinking is complete (has text content after) */
+ isComplete?: boolean
+}
+
+/**
+ * ThinkingPart component - renders thinking/reasoning content
+ *
+ * This component displays the model's internal reasoning process,
+ * typically shown in a collapsed or expandable format to distinguish
+ * it from the final response. It automatically collapses when thinking
+ * is complete.
+ *
+ * @example Standalone usage
+ * ```tsx
+ *
+ * ```
+ *
+ * @example Usage in partRenderers
+ * ```tsx
+ * (
+ *
+ * )
+ * }}
+ * />
+ * ```
+ */
+export function ThinkingPart({
+ content,
+ className = '',
+ isComplete = false,
+}: ThinkingPartProps) {
+ const [isCollapsed, setIsCollapsed] = useState(false)
+
+ // Auto-collapse when thinking completes
+ useEffect(() => {
+ if (isComplete) {
+ setIsCollapsed(true)
+ }
+ }, [isComplete])
+
+ return (
+
+
+ {!isCollapsed && (
+
+ {content}
+
+ )}
+
+ )
+}
diff --git a/packages/typescript/ai-preact-ui/src/tool-approval.tsx b/packages/typescript/ai-preact-ui/src/tool-approval.tsx
new file mode 100644
index 00000000..2f5073c1
--- /dev/null
+++ b/packages/typescript/ai-preact-ui/src/tool-approval.tsx
@@ -0,0 +1,129 @@
+import { useChatContext } from './chat'
+import type { ComponentChildren } from 'preact'
+
+export interface ToolApprovalProps {
+ /** Tool call ID */
+ toolCallId: string
+ /** Tool name */
+ toolName: string
+ /** Parsed tool arguments/input */
+ input: any
+ /** Approval metadata */
+ approval: {
+ id: string
+ needsApproval: boolean
+ approved?: boolean
+ }
+ /** CSS class name */
+ className?: string
+ /** Custom render prop */
+ children?: (props: ToolApprovalRenderProps) => ComponentChildren
+}
+
+export interface ToolApprovalRenderProps {
+ /** Tool name */
+ toolName: string
+ /** Parsed input */
+ input: any
+ /** Approve the tool call */
+ onApprove: () => void
+ /** Deny the tool call */
+ onDeny: () => void
+ /** Whether user has responded */
+ hasResponded: boolean
+ /** User's decision (if responded) */
+ approved?: boolean
+}
+
+/**
+ * Tool approval component - renders approve/deny buttons for tools that need approval
+ *
+ * @example
+ * ```tsx
+ * {part.approval && (
+ *
+ * )}
+ * ```
+ */
+export function ToolApproval({
+ toolCallId: _,
+ toolName,
+ input,
+ approval,
+ className,
+ children,
+}: ToolApprovalProps) {
+ const { addToolApprovalResponse } = useChatContext()
+
+ const handleApprove = () => {
+ addToolApprovalResponse({
+ id: approval.id,
+ approved: true,
+ })
+ }
+
+ const handleDeny = () => {
+ addToolApprovalResponse({
+ id: approval.id,
+ approved: false,
+ })
+ }
+
+ const hasResponded = approval.approved !== undefined
+
+ const renderProps: ToolApprovalRenderProps = {
+ toolName,
+ input,
+ onApprove: handleApprove,
+ onDeny: handleDeny,
+ hasResponded,
+ approved: approval.approved,
+ }
+
+ // Render prop pattern
+ if (children) {
+ return <>{children(renderProps)}>
+ }
+
+ // Already responded - show decision
+ if (hasResponded) {
+ return (
+
+ {approval.approved ? '✓ Approved' : '✗ Denied'}
+
+ )
+ }
+
+ // Default approval UI
+ return (
+
+
+ {toolName} requires approval
+
+
+
{JSON.stringify(input, null, 2)}
+
+
+
+
+
+
+ )
+}
diff --git a/packages/typescript/ai-preact-ui/tsconfig.json b/packages/typescript/ai-preact-ui/tsconfig.json
new file mode 100644
index 00000000..2ce68976
--- /dev/null
+++ b/packages/typescript/ai-preact-ui/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact",
+ "declaration": true,
+ "declarationMap": true,
+ "composite": true,
+ "lib": ["ES2022", "DOM"]
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/typescript/ai-preact-ui/vite.config.ts b/packages/typescript/ai-preact-ui/vite.config.ts
new file mode 100644
index 00000000..77bcc2e6
--- /dev/null
+++ b/packages/typescript/ai-preact-ui/vite.config.ts
@@ -0,0 +1,36 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import { tanstackViteConfig } from '@tanstack/vite-config'
+import packageJson from './package.json'
+
+const config = defineConfig({
+ test: {
+ name: packageJson.name,
+ dir: './',
+ watch: false,
+ globals: true,
+ environment: 'node',
+ include: ['tests/**/*.test.ts'],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html', 'lcov'],
+ exclude: [
+ 'node_modules/',
+ 'dist/',
+ 'tests/',
+ '**/*.test.ts',
+ '**/*.config.ts',
+ '**/types.ts',
+ ],
+ include: ['src/**/*.ts'],
+ },
+ },
+})
+
+export default mergeConfig(
+ config,
+ tanstackViteConfig({
+ entry: ['./src/index.ts'],
+ srcDir: './src',
+ cjs: false,
+ }),
+)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 21eb7cb9..4d05d95f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -658,7 +658,7 @@ importers:
version: 0.4.4(csstype@3.2.3)(solid-js@1.9.10)
'@tanstack/devtools-utils':
specifier: ^0.2.3
- version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))
+ version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))
goober:
specifier: ^2.1.18
version: 2.1.18(csstype@3.2.3)
@@ -774,6 +774,37 @@ importers:
specifier: ^7.2.7
version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
+ packages/typescript/ai-preact-ui:
+ dependencies:
+ '@tanstack/ai-preact':
+ specifier: workspace:*
+ version: link:../ai-preact
+ preact:
+ specifier: '>=10.11.0'
+ version: 10.28.0
+ preact-md:
+ specifier: ^0.2.1
+ version: 0.2.1(preact@10.28.0)
+ rehype-highlight:
+ specifier: ^7.0.2
+ version: 7.0.2
+ rehype-raw:
+ specifier: ^7.0.0
+ version: 7.0.0
+ remark-gfm:
+ specifier: ^4.0.1
+ version: 4.0.1
+ devDependencies:
+ '@tanstack/ai-client':
+ specifier: workspace:*
+ version: link:../ai-client
+ '@vitest/coverage-v8':
+ specifier: 4.0.14
+ version: 4.0.14(vitest@4.0.15(@types/node@25.0.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
+ vite:
+ specifier: ^7.2.7
+ version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
+
packages/typescript/ai-react:
dependencies:
'@tanstack/ai-client':
@@ -1025,7 +1056,7 @@ importers:
version: link:../ai-devtools
'@tanstack/devtools-utils':
specifier: ^0.2.3
- version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))
+ version: 0.2.3(@types/react@19.2.7)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))
preact:
specifier: ^10.0.0
version: 10.28.1
@@ -1044,7 +1075,7 @@ importers:
version: link:../ai-devtools
'@tanstack/devtools-utils':
specifier: ^0.2.3
- version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))
+ version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))
devDependencies:
'@types/react':
specifier: ^19.2.7
@@ -1175,7 +1206,7 @@ importers:
version: link:../ai-devtools
'@tanstack/devtools-utils':
specifier: ^0.2.3
- version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))
+ version: 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))
devDependencies:
'@vitest/coverage-v8':
specifier: 4.0.14
@@ -6558,12 +6589,20 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
- preact@10.28.2:
- resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==}
+ preact-md@0.2.1:
+ resolution: {integrity: sha512-IvZDZGix3v22iSqY0Bypt879WAh2uui6lyBCl4q3kafG2N/DwRpWUasFfg+ohXIa9xp6E+cyWc1H+6k7fYge0A==}
+ peerDependencies:
+ preact: ^10.0.0
+
+ preact@10.28.0:
+ resolution: {integrity: sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==}
preact@10.28.1:
resolution: {integrity: sha512-u1/ixq/lVQI0CakKNvLDEcW5zfCjUQfZdK9qqWuIJtsezuyG6pk9TWj75GMuI/EzRSZB/VAE43sNWWZfiy8psw==}
+ preact@10.28.2:
+ resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==}
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -9680,7 +9719,19 @@ snapshots:
transitivePeerDependencies:
- csstype
- '@tanstack/devtools-utils@0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))':
+ '@tanstack/devtools-utils@0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.2)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))':
+ dependencies:
+ '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10)
+ optionalDependencies:
+ '@types/react': 19.2.7
+ preact: 10.28.2
+ react: 19.2.3
+ solid-js: 1.9.10
+ vue: 3.5.25(typescript@5.9.3)
+ transitivePeerDependencies:
+ - csstype
+
+ '@tanstack/devtools-utils@0.2.3(@types/react@19.2.7)(preact@10.28.1)(react@19.2.3)(solid-js@1.9.10)(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10)
optionalDependencies:
@@ -14333,10 +14384,24 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
- preact@10.28.2: {}
+ preact-md@0.2.1(preact@10.28.0):
+ dependencies:
+ hast-util-sanitize: 5.0.2
+ marked: 17.0.1
+ preact: 10.28.0
+ rehype-sanitize: 6.0.0
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ preact@10.28.0: {}
preact@10.28.1: {}
+ preact@10.28.2: {}
+
prelude-ls@1.2.1: {}
premove@4.0.0: {}