npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

npm version License: MIT

📚 Table of Contents


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-runtime

Note: 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 messages
  • ToolCallMessage - Tool invocation messages
  • ToolResultMessage - Tool execution results
  • SystemMessage - System-level messages
  • StreamingMessage - 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 URLs

Document 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 button

Data Flow

  1. Message Sending Flow:

    User Input → MessageInput → sendMessage() → WebSocket
    → Backend → Claude API → WebSocket Response → useChatSession
    → State Update → MessageList → Re-render
  2. Session Management Flow:

    User Action → useSessions hook → SessionApi (REST)
    → Backend → State Update → UI Update
  3. Tool 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:

  1. Props drilling - For direct parent-child relationships
  2. Context API - For cross-cutting concerns (theme, tool results)
  3. Callbacks - For event handling (onSessionUpdate, onMessagesChange)
  4. 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 history
  • partial_assistant_message - Streaming assistant response
  • assistant_message - Complete assistant message
  • system_message - System-level message
  • status_message - Status update
  • hook_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:

📁 apps/example-backend/

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 bundlers

Best 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 (not http://)
  • For HTTPS sites, use wss:// instead of ws://
  • 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 supportImages and supportDocuments props are true
  • 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 apiBaseUrl is correct and backend is running
  • Check network tab for failed API requests
  • Ensure backend endpoints match expected format:
    • GET /api/sessions - List sessions
    • POST /api/sessions - Create session
    • GET /api/sessions/:id - Get session
    • DELETE /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-react

For 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


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:

  1. All message rendering components must be in the UI package - Ensures consistency and reusability
  2. Never use any types - Always use proper types from claude-agent-sdk for type safety
  3. Components are backend-agnostic - UI components don't dictate backend implementation

For more details, see CLAUDE.md in the repository root.