ai-core/chat-experience
This skill provides a complete implementation pattern for building a real-time chat application using TanStack's AI framework. It demonstrates server-side streaming chat endpoints with OpenAI integration and corresponding React client components that handle message display, user input, loading states, and stream cancellation through server-sent events.
git clone --depth 1 https://github.com/TanStack/ai /tmp/ai-core-chat-experience && cp -r /tmp/ai-core-chat-experience/packages/ai/skills/ai-core/chat-experience ~/.claude/skills/ai-core-chat-experienceSKILL.md
# Chat Experience
This skill builds on ai-core. Read it first for critical rules.
## Setup — Minimal Chat App
### Server: API Route (TanStack Start)
```typescript
// src/routes/api.chat.ts
import { createFileRoute } from '@tanstack/react-router'
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
const abortController = new AbortController()
const body = await request.json()
const { messages } = body
const stream = chat({
adapter: openaiText('gpt-5.2'),
messages,
systemPrompts: ['You are a helpful assistant.'],
abortController,
})
return toServerSentEventsResponse(stream, { abortController })
},
},
},
})
```
### Client: React Component
```typescript
// src/routes/index.tsx
import { useState } from 'react'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
import type { UIMessage } from '@tanstack/ai-react'
function ChatPage() {
const [input, setInput] = useState('')
const { messages, sendMessage, isLoading, error, stop } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
const handleSubmit = () => {
if (!input.trim()) return
sendMessage(input.trim())
setInput('')
}
return (
<div>
<div>
{messages.map((message: UIMessage) => (
<div key={message.id}>
<strong>{message.role}:</strong>
{message.parts.map((part, i) => {
if (part.type === 'text') {
return <p key={i}>{part.content}</p>
}
return null
})}
</div>
))}
</div>
{error && <div>Error: {error.message}</div>}
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
}}
disabled={isLoading}
placeholder="Type a message..."
/>
{isLoading ? (
<button onClick={stop}>Stop</button>
) : (
<button onClick={handleSubmit} disabled={!input.trim()}>
Send
</button>
)}
</div>
</div>
)
}
```
Vue/Solid/Svelte/Preact have identical patterns with different hook imports
(e.g., `import { useChat } from '@tanstack/ai-solid'`).
## Core Patterns
### 1. Streaming Chat with SSE
Server returns a streaming SSE Response; client parses it automatically.
**Server:**
```typescript
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
const stream = chat({
adapter: anthropicText('claude-sonnet-4-5'),
messages,
modelOptions: {
temperature: 0.7,
max_tokens: 2000, // Anthropic-native key
},
systemPrompts: ['You are a helpful assistant.'],
abortController,
})
return toServerSentEventsResponse(stream, { abortController })
```
**Client:**
```typescript
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
const { messages, sendMessage, isLoading, error, stop, status } = useChat({
connection: fetchServerSentEvents('/api/chat'),
body: { provider: 'anthropic', model: 'claude-sonnet-4-5' },
onFinish: (message) => {
console.log('Response complete:', message.id)
},
onError: (err) => {
console.error('Stream error:', err)
},
})
```
The `body` field is merged into the POST request body alongside `messages`,
letting the server read `data.provider`, `data.model`, etc.
The `status` field tracks the chat lifecycle: `'ready'` | `'submitted'` | `'streaming'` | `'error'`.
### 2. Rendering Thinking/Reasoning Content
Models with extended thinking (Claude, Gemini) emit `ThinkingPart` in the message parts array.
```typescript
import type { UIMessage } from '@tanstack/ai-react'
function MessageRenderer({ message }: { message: UIMessage }) {
return (
<div>
{message.parts.map((part, i) => {
if (part.type === 'thinking') {
const isComplete = message.parts
.slice(i + 1)
.some((p) => p.type === 'text')
return (
<details key={i} open={!isComplete}>
<summary>{isComplete ? 'Thought process' : 'Thinking...'}</summary>
<pre>{part.content}</pre>
</details>
)
}
if (part.type === 'text' && part.content) {
return <p key={i}>{part.content}</p>
}
if (part.type === 'tool-call') {
return (
<div key={part.id}>
Tool call: {part.name} ({part.state})
</div>
)
}
return null
})}
</div>
)
}
```
Server-side, enable thinking via `modelOptions` on the adapter:
```typescript
import { geminiText } from '@tanstack/ai-gemini'
const stream = chat({
adapter: geminiText('gemini-2.5-flash'),
messages,
modelOptions: {
thinkingConfig: {
includeThoughts: true,
thinkingBudget: 100,
},
},
})
```
### 3. Sending Multimodal Content (Images)
Use `sendMessage` with a `MultimodalContent` object instead of a plain string.
```typescript
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
import type { ContentPart } from '@tanstack/ai'
const { sendMessage } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
function sendImageMessage(text: string, imageBase64: string, mimeType: string) {
const contentParts: Array<ContentPart> = [
{ type: 'text', content: text },
{
type: 'image',
source: { type: 'data', value: imageBase64, mimeType },
},
]
sendMessage({ content: contentParts })
}
function sendImageUrl(text: string, imageUrl: string) {
const contentParts: Array>
Triage all open GitHub issues, PRs, and discussions in the current repository by fanning out up to 100 parallel subagents (one per item), then produce a single prioritized report ranking which PRs to review first, which issues to address first, and which discussions need maintainer attention. Use when the user asks to "triage open issues/PRs", "triage discussions", "prioritize the backlog", "what should I review first", "sweep the repo", or any request to bulk-evaluate open GitHub work and recommend an order.
>
>
>
>
>
>