> ## Documentation Index
> Fetch the complete documentation index at: https://docs.standardagentbuilder.com/llms.txt
> Use this file to discover all available pages before exploring further.

# @standardagents/react

> React hooks and components for AgentBuilder - connect to AI agent threads with real-time updates

## Overview

The `@standardagents/react` package provides React hooks and components for building UIs that connect to AgentBuilder threads. It handles real-time message streaming, custom event listening, and workblock transformation for tool visualization.

<Card title="Key Features" icon="react">
  * Real-time WebSocket updates for live message streaming
  * Context-based architecture with `ThreadProvider`
  * Workblock transformation for UI-friendly tool displays
  * Custom event system for backend-to-frontend communication
  * File uploads and attachments for sending images to the LLM
  * TypeScript support with full type definitions
  * Works with any React framework (Next.js, Remix, Vite, etc.)
</Card>

## Installation

```bash npm theme={null}
npm install @standardagents/react
```

```bash pnpm theme={null}
pnpm add @standardagents/react
```

```bash yarn theme={null}
yarn add @standardagents/react
```

## Quick Start

```tsx theme={null}
import {
  AgentBuilderProvider,
  ThreadProvider,
  useThread,
} from "@standardagents/react"

function App() {
  return (
    <AgentBuilderProvider config={{ endpoint: "https://your-api.com" }}>
      <ThreadProvider threadId="123e4567-e89b-12d3-a456-426614174000">
        <ChatInterface />
      </ThreadProvider>
    </AgentBuilderProvider>
  )
}

function ChatInterface() {
  const { messages, sendMessage, status } = useThread()

  const handleSend = async (text: string) => {
    await sendMessage({ role: "user", content: text })
  }

  return (
    <div>
      <p>Status: {status}</p>
      {messages.map((msg) => (
        <div key={msg.id}>
          <strong>{msg.role}:</strong> {msg.content}
        </div>
      ))}
      <input onKeyDown={(e) => e.key === "Enter" && handleSend(e.currentTarget.value)} />
    </div>
  )
}
```

## Core Providers

### AgentBuilderProvider

Root provider that configures the API endpoint for all child components.

```tsx theme={null}
<AgentBuilderProvider config={{ endpoint: "https://api.example.com" }}>
  {children}
</AgentBuilderProvider>
```

**Props:**

| Prop              | Type        | Required | Description          |
| ----------------- | ----------- | -------- | -------------------- |
| `config.endpoint` | `string`    | Yes      | The API endpoint URL |
| `children`        | `ReactNode` | Yes      | Child components     |

### ThreadProvider

Establishes a WebSocket connection to a specific thread and provides context for child components.

```tsx theme={null}
<ThreadProvider
  threadId="123e4567-e89b-12d3-a456-426614174000"
  preload={true}
  live={true}
  useWorkblocks={false}
  depth={0}
  includeSilent={false}
>
  <ChatInterface />
</ThreadProvider>
```

**Props:**

| Prop            | Type      | Default  | Description                                 |
| --------------- | --------- | -------- | ------------------------------------------- |
| `threadId`      | `string`  | Required | The thread ID to connect to                 |
| `preload`       | `boolean` | `true`   | Fetch existing messages on mount            |
| `live`          | `boolean` | `true`   | Enable WebSocket for live updates           |
| `useWorkblocks` | `boolean` | `false`  | Transform tool calls into workblocks        |
| `depth`         | `number`  | `0`      | Max message depth (0 = top-level only)      |
| `includeSilent` | `boolean` | `false`  | Include silent messages                     |
| `endpoint`      | `string`  | -        | Override endpoint from AgentBuilderProvider |

## Hooks

### useThread

Hook to access the full thread context. Must be used within a `ThreadProvider`.

```tsx theme={null}
import { useThread } from "@standardagents/react"

function ChatMessages() {
  const {
    // Messages & State
    messages,
    workblocks,
    loading,
    error,
    status,

    // Actions
    sendMessage,
    stopExecution,
    deleteMessage,

    // Custom Events
    onEvent,

    // File Management (uploads to filesystem)
    files,
    addFiles,
    removeFile,
    getFileUrl,
    getPreviewUrl,

    // Attachment Management (sent to LLM)
    attachments,
    addAttachment,
    removeAttachment,
    clearAttachments,
  } = useThread()

  return (
    <div>
      <p>Status: {status}</p>
      {loading && <p>Loading...</p>}
      {messages.map((msg) => (
        <MessageBubble key={msg.id} message={msg} />
      ))}
    </div>
  )
}
```

## Method Reference

### sendMessage

Send a message to the thread. Automatically includes any pending attachments and clears them after sending.

```tsx theme={null}
const { sendMessage, attachments } = useThread()

// Simple text message
await sendMessage({ role: 'user', content: 'Hello!' })

// With attachments (call addAttachment first)
// Attachments are auto-included and cleared after send
addAttachment(imageFile)
await sendMessage({ role: 'user', content: 'What is in this image?' })
```

**Signature:**

```typescript theme={null}
sendMessage(payload: Omit<SendMessagePayload, 'attachments'>): Promise<Message>

interface SendMessagePayload {
  role: 'user' | 'assistant' | 'system'
  content: string
  silent?: boolean
}
```

**Optimistic UI:** When you call `sendMessage()`, an optimistic message with attachment previews is shown immediately. The real message replaces it when confirmed by the server via WebSocket.

***

### addAttachment

Queue file(s) to be sent with the next message. Files are NOT uploaded immediately - they're held locally with preview URLs and sent inline (base64) when `sendMessage()` is called.

**Use this for images/files that should be sent directly to the LLM.**

```tsx theme={null}
const { addAttachment, attachments } = useThread()

// Single file
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
  if (e.target.files?.[0]) {
    addAttachment(e.target.files[0])
  }
}

// Multiple files
addAttachment(Array.from(fileInput.files))

// FileList directly
addAttachment(e.target.files)

// Show previews for pending attachments
{attachments.map(a => (
  <div key={a.id}>
    {a.isImage && a.previewUrl && (
      <img src={a.previewUrl} alt={a.name} />
    )}
    <span>{a.name}</span>
    <button onClick={() => removeAttachment(a.id)}>Remove</button>
  </div>
))}
```

**Signature:**

```typescript theme={null}
addAttachment(files: File | File[] | FileList): void
```

**PendingAttachment Type:**

```typescript theme={null}
interface PendingAttachment {
  id: string              // Temporary ID
  file: File              // Original file object
  name: string
  mimeType: string
  size: number
  isImage: boolean
  previewUrl: string | null  // Object URL for image preview
  width?: number
  height?: number
}
```

***

### removeAttachment

Remove a pending attachment before sending.

```tsx theme={null}
const { removeAttachment, attachments } = useThread()

<button onClick={() => removeAttachment(attachment.id)}>Remove</button>
```

**Signature:**

```typescript theme={null}
removeAttachment(id: string): void
```

***

### clearAttachments

Remove all pending attachments.

```tsx theme={null}
const { clearAttachments, attachments } = useThread()

{attachments.length > 0 && (
  <button onClick={clearAttachments}>Clear All</button>
)}
```

**Signature:**

```typescript theme={null}
clearAttachments(): void
```

***

### addFiles

Upload files to the thread's filesystem. Files are uploaded immediately and stored persistently.

**Use this for documents/files that should be stored on the thread but NOT sent directly to the LLM.**

```tsx theme={null}
const { addFiles, files, getPreviewUrl } = useThread()

// Upload files to filesystem
addFiles(fileInput.files)

// Display uploaded files
{files.map(f => (
  <div key={f.id}>
    {f.isImage && <img src={getPreviewUrl(f)} alt={f.name} />}
    <span>{f.name}</span>
    <span>{f.status}</span> {/* 'uploading' | 'ready' | 'committed' | 'error' */}
  </div>
))}
```

**Signature:**

```typescript theme={null}
addFiles(files: File[] | FileList): void
```

**ThreadFile Type:**

```typescript theme={null}
interface ThreadFile {
  id: string
  name: string
  mimeType: string
  size: number
  isImage: boolean
  localPreviewUrl: string | null
  status: 'uploading' | 'ready' | 'committed' | 'error'
  error?: string
  path?: string
  width?: number
  height?: number
  messageId?: string
}
```

***

### removeFile

Remove a pending file upload (cannot remove already committed files).

```tsx theme={null}
const { removeFile } = useThread()

<button onClick={() => removeFile(file.id)}>Cancel Upload</button>
```

***

### File URL Helpers

```tsx theme={null}
const { getFileUrl, getThumbnailUrl, getPreviewUrl } = useThread()

// Full file URL (for download/viewing)
const url = getFileUrl(file)

// Thumbnail URL (for images)
const thumb = getThumbnailUrl(file)

// Preview URL (smart - uses local preview for pending, thumbnail for committed)
const preview = getPreviewUrl(file)
```

***

### stopExecution

Stop the current agent execution.

```tsx theme={null}
const { stopExecution } = useThread()

<button onClick={stopExecution}>Stop</button>
```

***

### deleteMessage

Delete a message from the thread. The message is optimistically removed from the UI immediately. If the server request fails, the message is restored.

```tsx theme={null}
const { deleteMessage, messages } = useThread()

async function handleDelete(messageId: string) {
  try {
    await deleteMessage(messageId)
  } catch (err) {
    console.error('Failed to delete:', err)
  }
}

// Usage
{messages.map(msg => (
  <div key={msg.id}>
    <span>{msg.content}</span>
    <button onClick={() => handleDelete(msg.id)}>Delete</button>
  </div>
))}
```

**Signature:**

```typescript theme={null}
deleteMessage(messageId: string): Promise<void>
```

**Note:** Server-side attachment files are automatically cleaned up when a message is deleted.

***

### onEvent / subscribeToEvent

Subscribe to custom events emitted by the agent.

```tsx theme={null}
const { onEvent } = useThread()

useEffect(() => {
  const unsubscribe = onEvent<{ status: string }>('progress', (data) => {
    console.log('Progress:', data.status)
  })
  return unsubscribe
}, [])
```

## addFiles vs addAttachment

| Feature              | `addFiles()`                   | `addAttachment()`           |
| -------------------- | ------------------------------ | --------------------------- |
| **Purpose**          | Filesystem storage             | Send to LLM                 |
| **Upload timing**    | Immediate                      | On `sendMessage()`          |
| **Storage**          | Thread filesystem (persistent) | Inline base64               |
| **Image processing** | None (raw storage)             | Server-side resize/optimize |
| **Use case**         | Documents, persistent files    | Chat images to LLM          |

## Real-time File Synchronization

The `files` array from `useThread()` automatically stays synchronized with the server. When files are added, modified, or deleted on the thread (by the agent, tools, or other clients), the `files` array updates in real-time via WebSocket.

### File Events

The SDK listens for three file-related events:

| Event          | Description                                   |
| -------------- | --------------------------------------------- |
| `file_created` | A new file was added to the thread filesystem |
| `file_updated` | An existing file was modified                 |
| `file_deleted` | A file was removed from the thread filesystem |

### Custom File Event Handling

You can also subscribe to these events directly for custom handling:

```tsx theme={null}
const { onEvent } = useThread()

useEffect(() => {
  const unsubscribe = onEvent<{ path: string; file: { name: string; mimeType: string; size: number } }>('file_created', (data) => {
    console.log('File created:', data.path)
    // Custom notification, analytics, etc.
  })
  return unsubscribe
}, [onEvent])
```

### How It Works

1. **Initial load**: When `ThreadProvider` mounts, it fetches the current file list from the server
2. **Live updates**: WebSocket events update the `files` array as changes occur
3. **Optimistic UI**: Local uploads via `addFiles()` appear immediately as "uploading" then transition to "committed"
4. **Deduplication**: Server files take priority over local pending files with the same path

## useThread Return Value

**Full ThreadContextValue:**

| Property           | Type                                   | Description                                                     |
| ------------------ | -------------------------------------- | --------------------------------------------------------------- |
| `threadId`         | `string`                               | The current thread ID                                           |
| `messages`         | `Message[]`                            | All messages in the thread                                      |
| `workblocks`       | `ThreadMessage[]`                      | Messages transformed to workblocks (if `useWorkblocks` is true) |
| `loading`          | `boolean`                              | Whether messages are loading (alias: `isLoading`)               |
| `error`            | `Error \| null`                        | Any error that occurred                                         |
| `status`           | `ConnectionStatus`                     | WebSocket connection status (alias: `connectionStatus`)         |
| `options`          | `ThreadProviderOptions`                | Options passed to the provider                                  |
| `sendMessage`      | `(payload) => Promise<Message>`        | Send a message (auto-includes attachments)                      |
| `stopExecution`    | `() => Promise<void>`                  | Stop current execution                                          |
| `deleteMessage`    | `(messageId: string) => Promise<void>` | Delete a message (optimistic UI)                                |
| `onEvent`          | `<T>(type, listener) => () => void`    | Subscribe to custom events                                      |
| `files`            | `ThreadFile[]`                         | All files (pending uploads + committed)                         |
| `addFiles`         | `(files) => void`                      | Upload files to filesystem                                      |
| `removeFile`       | `(id) => void`                         | Remove a pending file                                           |
| `getFileUrl`       | `(file) => string`                     | Get file URL                                                    |
| `getThumbnailUrl`  | `(file) => string`                     | Get thumbnail URL                                               |
| `getPreviewUrl`    | `(file) => string \| null`             | Get preview URL                                                 |
| `attachments`      | `PendingAttachment[]`                  | Pending attachments for next message                            |
| `addAttachment`    | `(files) => void`                      | Add attachment(s) for next message                              |
| `removeAttachment` | `(id) => void`                         | Remove a pending attachment                                     |
| `clearAttachments` | `() => void`                           | Clear all pending attachments                                   |

**ConnectionStatus:**

```typescript theme={null}
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
```

### useThreadId

Returns the current thread ID from context.

```tsx theme={null}
import { useThreadId } from "@standardagents/react"

function ThreadInfo() {
  const threadId = useThreadId()
  return <span>Thread: {threadId}</span>
}
```

### Custom Events

Listen for custom events emitted by the backend using `onEvent` from `useThread()`:

```tsx theme={null}
import { useState, useEffect, useCallback } from "react"
import { useThread } from "@standardagents/react"

function GamePreview() {
  const { onEvent } = useThread()
  const [gameStatus, setGameStatus] = useState<{ success: boolean } | null>(null)

  useEffect(() => {
    // Subscribe to custom events - returns unsubscribe function
    const unsubscribe = onEvent<{ success: boolean }>('game_built', (data) => {
      setGameStatus(data)
    })
    return unsubscribe
  }, [onEvent])

  return gameStatus?.success ? <GameIframe /> : <LoadingSpinner />
}
```

**Backend Integration:**

Events are emitted from the backend using `emitThreadEvent()`:

```typescript theme={null}
// In a tool or hook on the backend
import { emitThreadEvent } from "@standardagents/builder"

emitThreadEvent(flow, "game_built", { success: true })
```

## Workblocks

Workblocks are a UI-friendly transformation of tool calls. When `useWorkblocks: true`, consecutive assistant messages with tool calls are grouped into workblocks.

<Tabs>
  <Tab title="Raw Messages">
    ```
    assistant (with tool_calls) → tool result → tool result → assistant (final)
    ```
  </Tab>

  <Tab title="Transformed Workblocks">
    ```
    workblock {
      workItems: [tool_call, tool_result, tool_call, tool_result]
      status: 'completed' | 'pending' | 'failed'
    } → assistant (final)
    ```
  </Tab>
</Tabs>

### WorkMessage Type

```typescript theme={null}
interface WorkMessage {
  id: string
  type: "workblock"
  content: string | null
  reasoning_content?: string | null
  workItems: WorkItem[]
  status: "pending" | "completed" | "failed"
  created_at: number
  depth?: number
}

interface WorkItem {
  id: string
  type: "tool_call" | "tool_result"
  name?: string
  content: string | null
  status?: "pending" | "success" | "error" | null
  tool_call_id?: string
}
```

### Rendering Workblocks

```tsx theme={null}
function MessageList() {
  const { workblocks } = useThread()

  return (
    <>
      {workblocks.map((msg) => {
        if ("type" in msg && msg.type === "workblock") {
          return <WorkBlockDisplay key={msg.id} workblock={msg} />
        }
        return <MessageBubble key={msg.id} message={msg} />
      })}
    </>
  )
}

function WorkBlockDisplay({ workblock }: { workblock: WorkMessage }) {
  return (
    <div className={`workblock ${workblock.status}`}>
      {workblock.workItems.map((item) => (
        <div key={item.id}>
          {item.type === "tool_call" ? `Calling ${item.name}...` : item.content}
        </div>
      ))}
    </div>
  )
}
```

## TypeScript Support

The package is written in TypeScript and includes full type definitions.

```tsx theme={null}
import type {
  // Core types
  Message,
  WorkMessage,
  WorkItem,
  ThreadMessage,
  Thread,

  // Configuration
  AgentBuilderConfig,
  UseThreadOptions,
  GetMessagesOptions,
  SendMessagePayload,
  ThreadProviderOptions,

  // Attachment types
  PendingAttachment,
  AttachmentPayload,
  AttachmentRef,
  ThreadFile,

  // WebSocket events
  MessageDataEvent,
  MessageChunkEvent,
  ErrorEvent,
  ThreadEvent,
  MessageStreamEvent,
  LogDataEvent,
  CustomEvent,
  StoppedByUserEvent,
  LogStreamEvent,

  // WebSocket callbacks
  MessageWebSocketCallbacks,
  LogWebSocketCallbacks,
} from "@standardagents/react"

// Connection status type
import type { ConnectionStatus } from "@standardagents/react"
```

### Message Type

```typescript theme={null}
interface Message {
  id: string
  role: "system" | "user" | "assistant" | "tool"
  content: string | null
  name?: string | null
  tool_calls?: string | null          // JSON array
  tool_call_id?: string | null
  log_id?: string | null
  created_at: number                  // microseconds
  request_sent_at?: number | null
  response_completed_at?: number | null
  status?: "pending" | "completed" | "failed"
  silent?: boolean
  tool_status?: "success" | "error" | null
  reasoning_content?: string | null
  reasoning_details?: string | null   // JSON array
  parent_id?: string | null
  depth?: number
  attachments?: string | null         // JSON array of AttachmentRef
}
```

## Complete Example

<Accordion title="View Complete Chat Interface Example with Image Attachments">
  ```tsx theme={null}
  import { useState } from "react"
  import {
    AgentBuilderProvider,
    ThreadProvider,
    useThread,
  } from "@standardagents/react"
  import type { ThreadMessage, Message, WorkMessage } from "@standardagents/react"

  function App() {
    const [threadId, setThreadId] = useState<string | null>(null)

    return (
      <AgentBuilderProvider config={{ endpoint: "/api" }}>
        {threadId ? (
          <ThreadProvider threadId={threadId} useWorkblocks>
            <ChatInterface />
          </ThreadProvider>
        ) : (
          <button onClick={() => createThread().then(setThreadId)}>
            Start Chat
          </button>
        )}
      </AgentBuilderProvider>
    )
  }

  function ChatInterface() {
    const {
      workblocks,
      sendMessage,
      stopExecution,
      status,
      loading,
      // Attachments for sending to LLM
      attachments,
      addAttachment,
      removeAttachment,
    } = useThread()

    const [input, setInput] = useState("")

    const isRunning = workblocks.some((m) => m.status === "pending")

    const handleSend = async () => {
      if (!input.trim() || isRunning) return
      await sendMessage({ role: "user", content: input })
      setInput("")
    }

    const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
      if (e.target.files?.length) {
        addAttachment(e.target.files)
      }
    }

    return (
      <div>
        <p>Status: {status} {loading && "(loading...)"}</p>

        <MessageList messages={workblocks} />

        {/* Pending attachments preview */}
        {attachments.length > 0 && (
          <div className="attachment-preview">
            {attachments.map((a) => (
              <div key={a.id} className="attachment-item">
                {a.isImage && a.previewUrl && (
                  <img src={a.previewUrl} alt={a.name} width={100} />
                )}
                <span>{a.name}</span>
                <button onClick={() => removeAttachment(a.id)}>×</button>
              </div>
            ))}
          </div>
        )}

        <div className="input-row">
          <input
            type="file"
            accept="image/*"
            multiple
            onChange={handleFileSelect}
          />
          <input
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && handleSend()}
            disabled={isRunning}
            placeholder="Type a message..."
          />
          {isRunning ? (
            <button onClick={stopExecution}>Stop</button>
          ) : (
            <button onClick={handleSend}>Send</button>
          )}
        </div>
      </div>
    )
  }

  function MessageList({ messages }: { messages: ThreadMessage[] }) {
    return (
      <div>
        {messages.map((msg) => {
          if ("type" in msg && msg.type === "workblock") {
            return <WorkBlock key={msg.id} block={msg} />
          }
          const message = msg as Message
          if (message.role === "tool") return null
          return (
            <div key={message.id} className={message.role}>
              {message.content}
              {message.attachments && (
                <MessageAttachments attachments={message.attachments} />
              )}
            </div>
          )
        })}
      </div>
    )
  }

  function MessageAttachments({ attachments }: { attachments: string }) {
    const parsed = JSON.parse(attachments) as AttachmentRef[]
    return (
      <div className="message-attachments">
        {parsed.map((a) => (
          <img
            key={a.id}
            src={a.localPreviewUrl || `/api/threads/.../fs/${a.path}`}
            alt={a.name}
          />
        ))}
      </div>
    )
  }

  function WorkBlock({ block }: { block: WorkMessage }) {
    return (
      <div className={`workblock ${block.status}`}>
        {block.workItems.map((item) => (
          <div key={item.id}>
            {item.type === "tool_call" ? (
              <span>Tool: {item.name}</span>
            ) : (
              <span>{item.content}</span>
            )}
          </div>
        ))}
      </div>
    )
  }
  ```
</Accordion>

## Next Steps

<CardGroup cols={2}>
  <Card title="Core Concepts" icon="book" href="/core/concepts">
    Learn about agents, prompts, and tools
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/react">
    Detailed API documentation
  </Card>

  <Card title="Builder Package" icon="cube" href="/packages/builder/overview">
    Backend framework documentation
  </Card>

  <Card title="Examples" icon="lightbulb" href="/examples">
    Browse example implementations
  </Card>
</CardGroup>
