@10play/claude-agent-sdk-ui
v0.1.2
Published
React UI components library for Claude Agent SDK
Readme
Claude Agent SDK UI
React UI components library for building chat applications with the Claude Agent SDK.
📚 Table of Contents
- Features
- Quick Start
- Component Overview
- Custom Components
- API Reference
- Component Architecture
- Styling & Customization
- Backend API Specification
- Common Patterns and Recipes
- Performance Optimization
- Troubleshooting
- Requirements
Features
- 🎨 Pre-built Components - Ready-to-use chat interface with streaming support
- 🔧 Flexible Architecture - Use the full app or individual components
- ✨ Custom Components - Override default message rendering with your own components
- 🎭 Theme Support - Built-in light/dark mode with customization
- 📎 Rich Attachments - Support for images and documents
- 🎤 Speech-to-Text - Built-in voice input support
- 🛠️ Tool Visualization - Display tool calls and results
- 💬 Command Autocomplete - Slash commands for enhanced UX
- 📦 TypeScript First - Full type safety with exported types
- ♿ Accessible - Built with accessibility in mind
Quick Start (30 seconds)
1. Install
npm install claude-agent-sdk-ui react react-dom lucide-react regenerator-runtimeNote: The Claude Agent SDK is bundled automatically—no need to install it separately!
2. Add Polyfills & Styles
// In your app entry point (main.tsx or App.tsx)
import 'regenerator-runtime/runtime' // Required for speech-to-text
import 'claude-agent-sdk-ui/styles.css'3. Use FullChatApp
import { FullChatApp } from 'claude-agent-sdk-ui'
function App() {
return (
<FullChatApp
apiBaseUrl="http://localhost:4001/api"
websocketUrl="ws://localhost:4001/ws"
/>
)
}That's it! You now have a fully functional chat application with session management, streaming, and tool support.
Component Overview
The library provides three levels of abstraction:
Level 1: Complete Application (Easiest)
<FullChatApp /> - A complete, batteries-included chat application
import { FullChatApp } from 'claude-agent-sdk-ui'
<FullChatApp
apiBaseUrl="http://localhost:4001/api"
websocketUrl="ws://localhost:4001/ws"
defaultTheme="dark"
supportImages={true}
supportDocuments={true}
/>Includes:
- Session list and management
- Chat interface with streaming
- Tool control panel
- Settings modal
- Theme toggle
- Import/export functionality
Level 2: Core Chat Interface (More Control)
<ChatInterface /> - Core chat UI without session management
import { ChatInterface, useSessions } from 'claude-agent-sdk-ui'
function MyChat() {
const { currentSession, ... } = useSessions({ sessionApi })
return (
<ChatInterface
session={currentSession}
websocketUrl="ws://localhost:4001/ws"
apiBaseUrl="http://localhost:4001/api"
onSessionUpdate={updateSession}
supportImages={true}
/>
)
}Level 3: Individual Components (Full Customization)
Build your own interface with granular components:
import {
MessageList,
MessageInput,
StreamingMessage,
ToolCallMessage,
ToolResultMessage,
ThemeToggle
} from 'claude-agent-sdk-ui'
function CustomChat() {
return (
<div>
<ThemeToggle />
<MessageList messages={messages}>
{messages.map(msg => (
<StreamingMessage key={msg.id} event={msg} />
))}
</MessageList>
<MessageInput onSendMessage={handleSend} />
</div>
)
}Custom Components
The library supports custom component overrides, allowing you to replace default message rendering components with your own implementations while maintaining full type safety and integration with the rest of the UI.
Why Use Custom Components?
- 🎨 Branding - Match your company's design system
- ✨ Enhanced Features - Add custom interactions like click-to-copy or inline editing
- 🎭 Different Layouts - Customize message bubble styles and positioning
- ♿ Accessibility - Add enhanced keyboard navigation or screen reader support
- 🔌 Integration - Connect messages to your existing component library
Available Component Overrides
You can override any of these message rendering components:
AssistantMessage- Assistant text messagesToolCallMessage- Tool invocation messagesToolResultMessage- Tool execution resultsSystemMessage- System-level messagesStreamingMessage- Streaming events
Basic Usage
import { FullChatApp, type CustomComponents } from 'claude-agent-sdk-ui'
// Define your custom components
const customComponents: CustomComponents = {
AssistantMessage: MyCustomAssistantMessage,
ToolCallMessage: MyCustomToolCallMessage,
// ... other components
}
function App() {
return (
<FullChatApp
apiBaseUrl="http://localhost:4001/api"
websocketUrl="ws://localhost:4001/ws"
components={customComponents}
/>
)
}Creating a Custom Component
All custom components receive properly typed props from the SDK:
import type { CustomAssistantMessageProps } from 'claude-agent-sdk-ui'
import { MarkdownEditor } from 'claude-agent-sdk-ui'
export function MyCustomAssistantMessage({
content,
isStreaming
}: CustomAssistantMessageProps) {
return (
<div className="my-custom-message">
<div className="custom-header">
🤖 AI Assistant
</div>
<MarkdownEditor
content={content}
isStreaming={isStreaming}
className="custom-content"
/>
</div>
)
}Component Props Types
The library exports all component prop types for type safety:
import type {
CustomAssistantMessageProps,
CustomToolCallMessageProps,
CustomToolResultMessageProps,
CustomSystemMessageProps,
CustomStreamingMessageProps
} from 'claude-agent-sdk-ui'Using with ChatInterface
Custom components work at all levels of the API:
import { ChatInterface, type CustomComponents } from 'claude-agent-sdk-ui'
const customComponents: CustomComponents = {
AssistantMessage: MyCustomAssistantMessage,
}
function MyChat() {
return (
<ChatInterface
session={session}
websocketUrl={wsUrl}
apiBaseUrl={apiUrl}
onSessionUpdate={updateSession}
components={customComponents}
/>
)
}Advanced Example: Custom Tool Result Message
import type { CustomToolResultMessageProps } from 'claude-agent-sdk-ui'
import { CollapsibleCodeBlock } from 'claude-agent-sdk-ui'
export function MyCustomToolResultMessage({
toolResult,
toolName,
timestamp
}: CustomToolResultMessageProps) {
// Extract content
let content = ''
if (typeof toolResult.content === 'string') {
content = toolResult.content
} else if (Array.isArray(toolResult.content)) {
content = toolResult.content
.map(item => item.text || JSON.stringify(item))
.join('\n')
}
return (
<div className="my-tool-result">
<div className="tool-header">
<span className="tool-name">{toolName}</span>
{toolResult.is_error && (
<span className="error-badge">Error</span>
)}
<span className="timestamp">
{new Date(timestamp).toLocaleTimeString()}
</span>
</div>
<CollapsibleCodeBlock
content={content}
language="text"
isError={toolResult.is_error}
/>
{/* Add custom actions */}
<button onClick={() => navigator.clipboard.writeText(content)}>
Copy Result
</button>
</div>
)
}Reusing Default Components
You can import and reuse the default components in your custom implementations:
import {
AssistantMessage,
ToolCallMessage,
MarkdownEditor,
CollapsibleCodeBlock
} from 'claude-agent-sdk-ui'
// Wrap the default component with additional features
export function EnhancedAssistantMessage(props) {
return (
<div className="enhanced-wrapper">
<button onClick={() => console.log('Message clicked')}>
⭐
</button>
<AssistantMessage {...props} />
</div>
)
}Notes
- All component overrides are optional - only override what you need
- Custom components receive the same props as the default components
- The library handles all message routing and state management
- You can use any React components, hooks, and patterns in your custom components
API Reference
Main Components
<FullChatApp />
Complete chat application with all features enabled.
Props:
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| apiBaseUrl | string | ✅ | - | REST API endpoint (e.g., "http://localhost:4001/api") |
| websocketUrl | string | ✅ | - | WebSocket endpoint (e.g., "ws://localhost:4001/ws") |
| defaultTheme | 'light' \| 'dark' | ❌ | 'light' | Initial theme |
| supportImages | boolean | ❌ | true | Enable image attachments |
| supportDocuments | boolean | ❌ | true | Enable document attachments (PDF, TXT) |
| defaultSettings | SettingsConfig | ❌ | See below | Default model and tool settings |
| components | CustomComponents | ❌ | undefined | Custom component overrides for message rendering |
Default Settings:
{
model: 'claude-sonnet-4-5-20250929',
allowedTools: ['Read', 'Edit', 'Bash', 'WebSearch', 'Write'],
maxBudgetUsd: undefined
}<ChatInterface />
Core chat UI component.
Props:
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| session | ChatSession \| null | ✅ | Current chat session |
| websocketUrl | string | ✅ | WebSocket endpoint |
| apiBaseUrl | string | ✅ | REST API endpoint |
| onSessionUpdate | (session: ChatSession) => void | ✅ | Callback when session updates |
| onMessagesChange | (messages: any[]) => void | ❌ | Callback when messages change |
| supportImages | boolean | ❌ | Enable image attachments |
| supportDocuments | boolean | ❌ | Enable document attachments |
| inputPlaceholder | string | ❌ | Custom input placeholder text |
| components | CustomComponents | ❌ | Custom component overrides for message rendering |
Message Input Components
Three variants available for different use cases:
<MessageInput /> - Simple text-only input
import { MessageInput } from 'claude-agent-sdk-ui'
<MessageInput
onSendMessage={(text) => console.log(text)}
disabled={false}
placeholder="Type a message..."
/><MessageInputWithImages /> - Text + image attachments
import { MessageInputWithImages } from 'claude-agent-sdk-ui'
<MessageInputWithImages
onSendMessage={(content) => {
console.log(content.text)
console.log(content.images) // ImageAttachment[]
}}
disabled={false}
/><MessageInputWithAttachments /> - Text + images + documents + speech
import { MessageInputWithAttachments } from 'claude-agent-sdk-ui'
<MessageInputWithAttachments
onSendMessage={(content) => {
console.log(content.text)
console.log(content.images) // ImageAttachment[]
console.log(content.documents) // DocumentAttachment[]
}}
disabled={false}
supportImages={true}
supportDocuments={true}
/>Message Display Components
<StreamingMessage /> - Displays streaming message events
import { StreamingMessage } from 'claude-agent-sdk-ui'
import type { StreamEvent } from '@anthropic-ai/claude-agent-sdk'
<StreamingMessage event={streamEvent} /><SystemMessage /> - System-level messages
import { SystemMessage } from 'claude-agent-sdk-ui'
<SystemMessage content="System initialized" /><ToolCallMessage /> - Tool invocation display
import { ToolCallMessage } from 'claude-agent-sdk-ui'
<ToolCallMessage
name="Read"
input={{ file_path: "/path/to/file.txt" }}
/><ToolResultMessage /> - Tool execution results
import { ToolResultMessage } from 'claude-agent-sdk-ui'
<ToolResultMessage
name="Read"
result={fileContents}
isError={false}
/>Hooks
useChatSession
Main hook for chat functionality with real-time WebSocket streaming.
import { useChatSession } from 'claude-agent-sdk-ui'
const {
messages, // Current messages
isStreaming, // Is currently streaming
sendMessage, // Send a message
stopStream, // Stop current stream
clearMessages, // Clear all messages
processingState // Processing status
} = useChatSession({
session,
websocketUrl: 'ws://localhost:4001/ws',
onMessagesChange: (msgs) => console.log(msgs)
})useSessions
Session management hook.
import { useSessions, createSessionApi } from 'claude-agent-sdk-ui'
const sessionApi = createSessionApi('http://localhost:4001/api')
const {
sessions, // All sessions
currentSession, // Current active session
loading, // Loading state
error, // Error state
createSession, // Create new session
deleteSession, // Delete a session
selectSession, // Switch to a session
updateSession // Update session
} = useSessions({ sessionApi })useCommandAutocomplete
Command autocomplete functionality.
import { useCommandAutocomplete, CommandService } from 'claude-agent-sdk-ui'
const commandService = new CommandService()
const {
showAutocomplete, // Should show autocomplete UI
filteredCommands, // Filtered command list
selectedIndex, // Currently selected index
selectCommand, // Select a command
handleKeyDown // Handle keyboard navigation
} = useCommandAutocomplete({
commandService,
inputValue: messageText
})useSpeechToText
Speech recognition hook.
import { useSpeechToText } from 'claude-agent-sdk-ui'
const {
transcript, // Current transcript
isListening, // Is currently listening
startListening, // Start recording
stopListening, // Stop recording
resetTranscript, // Clear transcript
browserSupportsSpeechRecognition
} = useSpeechToText()Contexts
ThemeProvider / useTheme
Theme management.
import { ThemeProvider, useTheme } from 'claude-agent-sdk-ui'
function App() {
return (
<ThemeProvider defaultTheme="dark">
<YourApp />
</ThemeProvider>
)
}
function ThemeToggleButton() {
const { theme, setTheme } = useTheme()
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
)
}ToolResultProvider / useToolResultContext
Tool result state management.
import { ToolResultProvider, useToolResultContext } from 'claude-agent-sdk-ui'
function App() {
return (
<ToolResultProvider>
<YourApp />
</ToolResultProvider>
)
}
function ToolPanel() {
const { toolResults, clearToolResults } = useToolResultContext()
// ...
}Utilities
Session Export/Import
import { downloadSessionAsJson, parseSessionImport } from 'claude-agent-sdk-ui'
// Export session
downloadSessionAsJson(messages, 'my-chat.json')
// Import session
const jsonString = await file.text()
const { messages, title } = parseSessionImport(jsonString)Image Utilities
import {
fileToImageAttachment,
isValidImageFile,
revokeImagePreview
} from 'claude-agent-sdk-ui'
const attachment = await fileToImageAttachment(file)
const isValid = isValidImageFile(file)
revokeImagePreview(attachment) // Clean up blob URLsDocument Utilities
import {
fileToDocumentAttachment,
isValidDocumentFile,
formatFileSize
} from 'claude-agent-sdk-ui'
const doc = await fileToDocumentAttachment(file)
const isValid = isValidDocumentFile(file) // PDF, TXT
const size = formatFileSize(file.size) // "1.5 MB"Component Architecture & Composition
Understanding how components work together will help you build better applications and troubleshoot issues effectively.
Component Hierarchy
FullChatApp
├── ThemeProvider
├── ToolResultProvider
├── SessionList (sidebar)
│ ├── SessionItem (multiple)
│ └── NewSessionButton
└── ChatInterface
├── EditableTitle
├── WorkingDirectorySelector
├── MessageList
│ ├── ProcessingIndicator
│ ├── MessageBubble (multiple)
│ │ ├── AssistantMessage
│ │ │ └── MarkdownEditor
│ │ │ └── StreamingText
│ │ ├── ToolCallResultPair
│ │ │ ├── ToolCallMessage
│ │ │ │ └── CollapsibleCodeBlock
│ │ │ └── ToolResultMessage
│ │ │ └── CollapsibleCodeBlock / JsonTreeViewer
│ │ ├── SystemMessage
│ │ └── StreamingMessage
│ └── ThoughtProcess (tool panel)
│ └── ToolControlPanel
│ └── TodoListViewer
└── MessageInputWithAttachments
├── CommandAutocomplete
├── ImagePreview
├── DocumentPreview
└── SpeechToText buttonData Flow
Message Sending Flow:
User Input → MessageInput → sendMessage() → WebSocket → Backend → Claude API → WebSocket Response → useChatSession → State Update → MessageList → Re-renderSession Management Flow:
User Action → useSessions hook → SessionApi (REST) → Backend → State Update → UI UpdateTool Execution Flow:
Claude → tool_use → ToolCallMessage rendered → Backend executes tool → tool_result → ToolResultMessage rendered → ToolResultContext updated
Context Providers
The library uses React Context for shared state:
ThemeProvider
- Purpose: Manages light/dark theme state
- Persisted: Yes (localStorage)
- Usage: Wrap your app with
<ThemeProvider> - Access: Use
useTheme()hook
ToolResultProvider
- Purpose: Tracks tool execution results for the control panel
- Persisted: No (session-scoped)
- Usage: Wrap your app with
<ToolResultProvider> - Access: Use
useToolResultContext()hook
Component Communication
Components communicate through:
- Props drilling - For direct parent-child relationships
- Context API - For cross-cutting concerns (theme, tool results)
- Callbacks - For event handling (onSessionUpdate, onMessagesChange)
- WebSocket events - For real-time streaming from backend
State Management
The library uses React hooks for state management:
- useChatSession - Manages chat messages, streaming, and WebSocket connection
- useSessions - Manages session list and CRUD operations
- useCommandAutocomplete - Manages slash command autocomplete state
- useSpeechToText - Manages voice input state
Styling & Customization
The library uses Tailwind CSS with CSS variables for theming, making it easy to customize the appearance.
CSS Variables
All colors and spacing can be customized via CSS variables:
:root {
/* Background colors */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* Component colors */
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
/* Primary brand color */
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
/* Secondary colors */
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
/* Muted colors for less important content */
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
/* Accent colors for highlights */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
/* Destructive colors for errors */
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
/* Border and input */
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
/* Border radius */
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... dark theme colors */
}Custom Theme Example
Create your own theme by overriding CSS variables:
/* custom-theme.css */
:root {
/* Brand colors */
--primary: 262 83% 58%; /* Purple */
--primary-foreground: 0 0% 100%;
/* Custom accent */
--accent: 173 80% 40%; /* Teal */
--accent-foreground: 0 0% 100%;
/* Rounded corners */
--radius: 1rem;
}
.dark {
--background: 240 10% 3.9%; /* Deeper dark */
--primary: 263 70% 50%; /* Adjusted purple for dark mode */
}Then import it after the main styles:
import 'claude-agent-sdk-ui/styles.css'
import './custom-theme.css'Component-Specific Styling
Target specific components with Tailwind classes:
import { FullChatApp } from 'claude-agent-sdk-ui'
<div className="my-chat-wrapper">
<FullChatApp {...props} />
</div>/* Override specific component styles */
.my-chat-wrapper {
/* Message bubbles */
--muted: 210 40% 98%;
/* Adjust spacing */
--spacing-message: 1rem;
}
/* Target internal components (use with caution) */
.my-chat-wrapper .message-bubble {
@apply shadow-lg;
}Dark Mode Customization
The library automatically applies dark mode classes. You can customize dark mode separately:
.dark {
/* Override dark mode colors */
--background: 220 15% 8%; /* Custom dark background */
--card: 220 15% 12%; /* Slightly lighter cards */
--border: 220 15% 20%; /* Visible borders in dark mode */
}Markdown Styling
Customize markdown rendering in messages:
/* Override markdown styles */
.markdown-editor {
/* Code blocks */
pre {
@apply bg-muted rounded-lg p-4;
}
/* Inline code */
code {
@apply bg-accent text-accent-foreground px-1 rounded;
}
/* Headings */
h1, h2, h3 {
@apply font-bold text-foreground;
}
/* Links */
a {
@apply text-primary hover:underline;
}
}Backend API Specification
The UI library expects your backend to implement the following REST API endpoints and WebSocket protocol.
REST API Endpoints
Sessions Management
GET /api/sessions
- Description: List all sessions
- Response:
{
sessions: Array<{
id: string
title: string
created_at: string
updated_at: string
metadata?: {
working_directory?: string
model?: string
[key: string]: any
}
}>
}POST /api/sessions
- Description: Create a new session
- Request Body:
{
title?: string
metadata?: {
working_directory?: string
model?: string
[key: string]: any
}
}- Response:
{
session: {
id: string
title: string
created_at: string
updated_at: string
metadata?: object
}
}GET /api/sessions/:id
- Description: Get a specific session with messages
- Response:
{
session: {
id: string
title: string
created_at: string
updated_at: string
metadata?: object
},
messages: Array<{
id: string
session_id: string
sdk_message: SDKMessage // From @anthropic-ai/claude-agent-sdk
timestamp: string
}>
}PUT /api/sessions/:id
- Description: Update session (title, metadata)
- Request Body:
{
title?: string
metadata?: object
}- Response:
{
session: {
id: string
title: string
updated_at: string
metadata?: object
}
}DELETE /api/sessions/:id
- Description: Delete a session
- Response:
{
success: boolean
}POST /api/sessions/:id/messages
- Description: Load messages into a session (for import)
- Request Body:
{
title?: string
messages: Array<SDKMessage>
}- Response:
{
session: {
id: string
title: string
updated_at: string
}
}Settings Management
GET /api/sessions/:id/settings
- Description: Get session settings (model, tools, budget)
- Response:
{
model: string
allowed_tools: string[]
max_budget_usd?: number
}PUT /api/sessions/:id/settings
- Description: Update session settings
- Request Body:
{
model?: string
allowed_tools?: string[]
max_budget_usd?: number
}- Response:
{
settings: {
model: string
allowed_tools: string[]
max_budget_usd?: number
}
}WebSocket Protocol
The WebSocket connection is used for real-time message streaming.
Connection
Endpoint: ws://your-backend/ws
Query Parameters:
sessionId- The session ID for this connection
Example: ws://localhost:4001/ws?sessionId=abc123
Client → Server Messages
Send Message:
{
type: 'message',
sessionId: string,
content: string | Array<ContentBlock> // Text or multimodal content
}Stop Stream:
{
type: 'stop',
sessionId: string
}Update Title:
{
type: 'update_title',
sessionId: string,
title: string
}Server → Client Messages
The server should send StreamEvent objects from the Claude Agent SDK:
{
event: SDKStreamEvent // From @anthropic-ai/claude-agent-sdk
}Event Types:
user_message- User message added to historypartial_assistant_message- Streaming assistant responseassistant_message- Complete assistant messagesystem_message- System-level messagestatus_message- Status updatehook_response_message- Hook execution result
Error Messages:
{
error: string,
details?: any
}Example WebSocket Flow
Client connects: ws://localhost:4001/ws?sessionId=abc123
Client → Server:
{
"type": "message",
"sessionId": "abc123",
"content": "Hello Claude!"
}
Server → Client (multiple events):
{
"event": {
"type": "user_message",
"message": { ... }
}
}
{
"event": {
"type": "partial_assistant_message",
"message": { "content": "Hello! " }
}
}
{
"event": {
"type": "partial_assistant_message",
"message": { "content": "How can I help?" }
}
}
{
"event": {
"type": "assistant_message",
"message": { ... complete message ... }
}
}Reference Implementation
For a complete, working backend implementation, see the example backend in the repository:
The example backend includes:
- Express.js server with all endpoints
- WebSocket server implementation
- Claude Agent SDK integration
- Session persistence (file-based)
- MCP server support
- TypeScript with full type safety
Performance Optimization
Tips and best practices for optimal performance with large chat histories and complex applications.
Message Virtualization
For chats with hundreds of messages, consider implementing virtualization:
import { useChatSession } from 'claude-agent-sdk-ui'
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualizedChat({ session }) {
const { messages } = useChatSession({ session, websocketUrl })
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // Estimated message height
overscan: 5
})
return (
<div ref={parentRef} className="h-screen overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative'
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const message = messages[virtualRow.index]
return (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start}px)`
}}
>
<MessageBubble message={message} />
</div>
)
})}
</div>
</div>
)
}Pagination
Implement message pagination for large sessions:
function PaginatedChat({ session }) {
const [page, setPage] = useState(0)
const [pageSize] = useState(50)
const { messages } = useChatSession({ session, websocketUrl })
// Show only recent messages
const visibleMessages = messages.slice(-pageSize * (page + 1))
return (
<div>
{messages.length > pageSize && (
<button onClick={() => setPage(p => p + 1)}>
Load Earlier Messages
</button>
)}
<MessageList messages={visibleMessages} />
</div>
)
}Debounced Updates
Debounce expensive operations like title updates:
import { useDebouncedCallback } from 'use-debounce'
function DebouncedTitleEdit() {
const debouncedUpdate = useDebouncedCallback(
(newTitle: string) => {
updateTitle(newTitle)
},
1000 // Wait 1 second after user stops typing
)
return (
<input
onChange={(e) => debouncedUpdate(e.target.value)}
placeholder="Session title"
/>
)
}Memoization
Use React memoization for expensive computations:
import { useMemo } from 'react'
function ChatStats({ messages }) {
const stats = useMemo(() => {
return {
totalMessages: messages.length,
userMessages: messages.filter(m => m.sdk_message.type === 'user').length,
toolCalls: messages.filter(m =>
m.sdk_message.type === 'assistant' &&
m.sdk_message.message?.content?.some(c => c.type === 'tool_use')
).length
}
}, [messages])
return <div>{/* Display stats */}</div>
}Image Optimization
Optimize image attachments before sending:
async function optimizeImage(file: File): Promise<File> {
// Resize if too large
if (file.size > 5 * 1024 * 1024) { // 5MB
const img = await createImageBitmap(file)
const canvas = document.createElement('canvas')
// Scale down to max 1920px width
const scale = Math.min(1, 1920 / img.width)
canvas.width = img.width * scale
canvas.height = img.height * scale
const ctx = canvas.getContext('2d')!
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
// Convert to blob
const blob = await new Promise<Blob>((resolve) => {
canvas.toBlob((b) => resolve(b!), 'image/jpeg', 0.85)
})
return new File([blob], file.name, { type: 'image/jpeg' })
}
return file
}Cleanup & Memory Management
Clean up blob URLs to prevent memory leaks:
import { useEffect } from 'react'
import { revokeImagePreview } from 'claude-agent-sdk-ui'
function ImageMessage({ attachment }) {
useEffect(() => {
// Cleanup when component unmounts
return () => {
if (attachment.preview) {
revokeImagePreview(attachment)
}
}
}, [attachment])
return <img src={attachment.preview} alt="" />
}WebSocket Reconnection
Implement robust reconnection logic:
function useRobustWebSocket(url: string) {
const [reconnectAttempts, setReconnectAttempts] = useState(0)
const maxRetries = 5
useEffect(() => {
let ws: WebSocket
let reconnectTimer: NodeJS.Timeout
const connect = () => {
ws = new WebSocket(url)
ws.onclose = () => {
if (reconnectAttempts < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)
reconnectTimer = setTimeout(() => {
setReconnectAttempts(a => a + 1)
connect()
}, delay)
}
}
ws.onopen = () => {
setReconnectAttempts(0) // Reset on successful connection
}
}
connect()
return () => {
clearTimeout(reconnectTimer)
ws?.close()
}
}, [url, reconnectAttempts])
}Bundle Size Optimization
If you're not using certain features, you can reduce bundle size:
// Instead of importing everything
import { FullChatApp } from 'claude-agent-sdk-ui' // ~500KB
// Import only what you need
import { ChatInterface } from 'claude-agent-sdk-ui/components/ChatInterface'
import { MessageInput } from 'claude-agent-sdk-ui/components/MessageInput'
// This may reduce bundle size slightly
// Note: Tree-shaking should handle this automatically with modern bundlersBest Practices Summary
✅ Do:
- Use virtualization for 100+ messages
- Implement pagination for very long sessions
- Debounce user input updates
- Memoize expensive computations
- Clean up blob URLs and subscriptions
- Implement WebSocket reconnection logic
- Optimize images before upload
❌ Don't:
- Render all messages unconditionally
- Make API calls on every keystroke
- Keep large message histories in memory
- Forget to cleanup event listeners
- Send unoptimized large images
Common Patterns and Recipes
1. Simple Text-Only Chat
import { FullChatApp } from 'claude-agent-sdk-ui'
import 'claude-agent-sdk-ui/styles.css'
function App() {
return (
<FullChatApp
apiBaseUrl="http://localhost:4001/api"
websocketUrl="ws://localhost:4001/ws"
supportImages={false}
supportDocuments={false}
/>
)
}2. Dark Mode by Default
<FullChatApp
apiBaseUrl="http://localhost:4001/api"
websocketUrl="ws://localhost:4001/ws"
defaultTheme="dark"
/>3. Custom Tool Configuration
<FullChatApp
apiBaseUrl="http://localhost:4001/api"
websocketUrl="ws://localhost:4001/ws"
defaultSettings={{
model: 'claude-sonnet-4-5-20250929',
allowedTools: ['Read', 'Write', 'Bash'],
maxBudgetUsd: 1.0
}}
/>4. Building a Custom Chat Interface
import {
ChatInterface,
ThemeProvider,
ToolResultProvider,
useSessions,
createSessionApi
} from 'claude-agent-sdk-ui'
import 'claude-agent-sdk-ui/styles.css'
function CustomChatApp() {
const sessionApi = createSessionApi('http://localhost:4001/api')
const { currentSession, updateSession } = useSessions({ sessionApi })
return (
<ThemeProvider defaultTheme="light">
<ToolResultProvider>
<div className="custom-layout">
<header>My Custom Chat</header>
<ChatInterface
session={currentSession}
websocketUrl="ws://localhost:4001/ws"
apiBaseUrl="http://localhost:4001/api"
onSessionUpdate={updateSession}
supportImages={true}
/>
</div>
</ToolResultProvider>
</ThemeProvider>
)
}5. Using Individual Components
import {
MessageInput,
StreamingMessage,
ThemeToggle,
useChatSession
} from 'claude-agent-sdk-ui'
import 'claude-agent-sdk-ui/styles.css'
function MinimalChat({ session }) {
const {
messages,
sendMessage,
isStreaming
} = useChatSession({
session,
websocketUrl: 'ws://localhost:4001/ws'
})
return (
<div className="flex flex-col h-screen bg-background">
<header className="flex justify-between p-4 border-b">
<h1>Chat</h1>
<ThemeToggle />
</header>
<div className="flex-1 overflow-y-auto p-4">
{messages.map((msg, idx) => (
<StreamingMessage key={idx} event={msg} />
))}
</div>
<MessageInput
onSendMessage={sendMessage}
disabled={isStreaming}
/>
</div>
)
}6. Handling Tool Results
import { useToolResultContext } from 'claude-agent-sdk-ui'
function ToolResultsPanel() {
const { toolResults, clearToolResults } = useToolResultContext()
return (
<div className="tool-results">
<h3>Tool Results ({toolResults.length})</h3>
<button onClick={clearToolResults}>Clear</button>
{toolResults.map((result, idx) => (
<div key={idx}>
<strong>{result.name}</strong>: {result.status}
</div>
))}
</div>
)
}7. Custom Message Handling
import { useChatSession } from 'claude-agent-sdk-ui'
function ChatWithLogging({ session }) {
const { sendMessage, messages } = useChatSession({
session,
websocketUrl: 'ws://localhost:4001/ws',
onMessagesChange: (msgs) => {
// Custom logging or analytics
console.log('Messages updated:', msgs.length)
analytics.track('messages_changed', { count: msgs.length })
}
})
const handleSend = async (text: string) => {
// Pre-process message
const processed = text.trim().toLowerCase()
// Send to chat
await sendMessage(processed)
// Post-process
console.log('Message sent:', processed)
}
return <MessageInput onSendMessage={handleSend} />
}8. Implementing Session Persistence
import { downloadSessionAsJson, parseSessionImport } from 'claude-agent-sdk-ui'
function ChatWithPersistence() {
const handleExport = () => {
// Export current session
downloadSessionAsJson(messages, `chat-${Date.now()}.json`)
}
const handleImport = async (file: File) => {
const text = await file.text()
const { messages, title } = parseSessionImport(text)
// Load into session
await sessionApi.loadMessages({
title: title || 'Imported Session',
messages
})
}
return (
<>
<button onClick={handleExport}>Export Chat</button>
<input
type="file"
accept=".json"
onChange={(e) => e.target.files?.[0] && handleImport(e.target.files[0])}
/>
</>
)
}Troubleshooting
Common Issues
1. Styles not loading
Problem: Components appear unstyled
Solution: Make sure to import the CSS file:
import 'claude-agent-sdk-ui/styles.css'Add this to your app's entry point (usually main.tsx or App.tsx).
2. WebSocket connection fails
Problem: WebSocket connection failed error
Solutions:
- Verify your backend server is running
- Check the WebSocket URL format:
ws://localhost:4001/ws(nothttp://) - For HTTPS sites, use
wss://instead ofws:// - Check CORS configuration on your backend
// Development
<FullChatApp websocketUrl="ws://localhost:4001/ws" />
// Production (HTTPS)
<FullChatApp websocketUrl="wss://your-domain.com/ws" />3. TypeScript errors with message types
Problem: Type errors when working with messages
Solution: Import proper types from the SDK:
import type { StreamEvent, SDKMessage } from '@anthropic-ai/claude-agent-sdk'
// Never use 'any' - always use proper types
const messages: StreamEvent[] = [] // ✅ Good
const messages: any[] = [] // ❌ Bad (violates architecture guidelines)4. Image/document attachments not working
Problem: Files aren't uploading or displaying
Solutions:
- Ensure
supportImagesandsupportDocumentsprops aretrue - Check file size limits (browser-dependent, typically 10MB)
- Verify file types:
- Images: PNG, JPEG, GIF, WebP
- Documents: PDF, TXT
- Check browser console for errors
<FullChatApp
supportImages={true} // ✅ Enable images
supportDocuments={true} // ✅ Enable documents
/>5. Theme not persisting
Problem: Theme resets on page reload
Solution: Implement theme persistence:
import { ThemeProvider } from 'claude-agent-sdk-ui'
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
// Load from localStorage
return (localStorage.getItem('theme') as 'light' | 'dark') || 'light'
})
useEffect(() => {
// Save to localStorage
localStorage.setItem('theme', theme)
}, [theme])
return (
<ThemeProvider defaultTheme={theme}>
<FullChatApp {...props} />
</ThemeProvider>
)
}6. Sessions not loading
Problem: Session list is empty or not loading
Solutions:
- Verify
apiBaseUrlis correct and backend is running - Check network tab for failed API requests
- Ensure backend endpoints match expected format:
GET /api/sessions- List sessionsPOST /api/sessions- Create sessionGET /api/sessions/:id- Get sessionDELETE /api/sessions/:id- Delete session
7. Command autocomplete not showing
Problem: Slash commands don't trigger autocomplete
Solutions:
- Type
/at the start of the message - Ensure you're using a component with command support:
MessageInput✅MessageInputWithAttachments✅- Custom input ❌ (unless you implement it)
8. Build errors with Vite/Webpack
Problem: Build fails with import errors
Solution: Ensure peer dependencies are installed:
npm install react react-dom lucide-reactFor Vite, add to vite.config.ts:
export default defineConfig({
optimizeDeps: {
include: ['claude-agent-sdk-ui']
}
})9. Speech-to-text not working
Problem: Microphone button doesn't work
Solutions:
- Only works over HTTPS (or localhost)
- Browser must support Web Speech API (Chrome, Edge, Safari)
- User must grant microphone permissions
- Check browser console for permission errors
import { useSpeechToText } from 'claude-agent-sdk-ui'
const { browserSupportsSpeechRecognition } = useSpeechToText()
if (!browserSupportsSpeechRecognition) {
console.warn('Speech recognition not supported')
}10. Performance issues with large chat histories
Problem: UI becomes slow with many messages
Solutions:
- Implement message pagination
- Limit rendered messages with windowing
- Use React virtualization libraries for long lists
import { useChatSession } from 'claude-agent-sdk-ui'
function OptimizedChat({ session }) {
const { messages } = useChatSession({ session, websocketUrl })
// Only render last 100 messages
const recentMessages = messages.slice(-100)
return (
<MessageList>
{recentMessages.map(msg => (
<StreamingMessage key={msg.id} event={msg} />
))}
</MessageList>
)
}Getting Help
- GitHub Issues: Report bugs or request features
- Discussions: Ask questions and share ideas
- Documentation: Check the main repo README for architecture guidelines
Requirements
- React 18+
- TypeScript 5+
- Modern browser with ES2020 support
Peer Dependencies
{
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"lucide-react": ">=0.294.0"
}Note: The Claude Agent SDK (
@anthropic-ai/claude-agent-sdk) is included as a direct dependency—you don't need to install it separately!
License
MIT © 10Play
Contributing
Contributions are welcome! Please read the contributing guidelines in the main repository.
Architecture Guidelines
This library follows strict architectural principles:
- All message rendering components must be in the UI package - Ensures consistency and reusability
- Never use
anytypes - Always use proper types fromclaude-agent-sdkfor type safety - Components are backend-agnostic - UI components don't dictate backend implementation
For more details, see CLAUDE.md in the repository root.
