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

# React

> React hooks and provider for AI SDK 5 chat UIs

The `@openharness/react` package provides hooks that wire into AI SDK 5's `useChat` and track OpenHarness-specific state like subagent activity, compaction, and turn lifecycle.

## Setup

```bash theme={"dark"}
npm install @openharness/react
```

Wrap your app (or chat component) with `OpenHarnessProvider`:

```tsx theme={"dark"}
import { OpenHarnessProvider } from "@openharness/react";

function App() {
  return (
    <OpenHarnessProvider>
      <Chat />
    </OpenHarnessProvider>
  );
}
```

## `useOpenHarness`

Creates a chat session connected to your API endpoint. Returns the same interface as AI SDK 5's `useChat` (`messages`, `sendMessage`, `status`, `stop`, etc.), typed with `OHUIMessage`:

```tsx theme={"dark"}
import { useOpenHarness } from "@openharness/react";

function Chat() {
  const { messages, sendMessage, status, stop } = useOpenHarness({
    endpoint: "/api/chat",
  });

  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>
          {msg.parts.map((part, i) =>
            part.type === "text" ? <span key={i}>{part.text}</span> : null
          )}
        </div>
      ))}
      <button onClick={() => sendMessage({ text: "Hello" })}>Send</button>
    </div>
  );
}
```

### Resuming Streams

Pass a stable `id` and `resume: true` to reconnect to an active server-side stream when the component mounts:

```tsx theme={"dark"}
const chat = useOpenHarness({
  endpoint: "/api/tasks/task-1/chat",
  id: "task-1",
  resume: true,
  prepareReconnectToStreamRequest: ({ id }) => ({
    api: `/api/tasks/${id}/chat/stream`,
    credentials: "include",
  }),
});
```

By default, reconnect attempts use `GET ${endpoint}/${id}/stream`. The server must own the active run and return the active stream or `204` when no stream is running.

<Note>
  AI SDK stream resumption is not compatible with treating browser request abort as cancellation. If a user needs to stop a background run, expose a separate server-side cancel action.
</Note>

## `useSubagentStatus`

Derives reactive state from `data-oh:subagent.*` events:

```tsx theme={"dark"}
import { useSubagentStatus } from "@openharness/react";

function SubagentDisplay() {
  const { activeSubagents, recentSubagents, hasActiveSubagents } = useSubagentStatus();

  if (!hasActiveSubagents) return null;

  return (
    <div>
      {activeSubagents.map((agent) => (
        <div key={agent.path.join("/")}>
          {agent.name}: {agent.task}
        </div>
      ))}
    </div>
  );
}
```

* **`activeSubagents`** — currently running subagents
* **`recentSubagents`** — all subagents seen in this session
* **`hasActiveSubagents`** — boolean shorthand

## `useSessionStatus`

Tracks turn index, compaction state, and retry info from `data-oh:*` events:

```tsx theme={"dark"}
import { useSessionStatus } from "@openharness/react";

function SessionInfo() {
  const session = useSessionStatus();

  return (
    <div>
      Turn: {session.turnIndex} | Messages: {session.messageCount}
    </div>
  );
}
```

## `useTodos`

Tracks the latest todo list from `data-oh:todo.updated` events emitted by the core todo tools:

```tsx theme={"dark"}
import { useTodos } from "@openharness/react";

function TodoDock() {
  const { todos, activeTodo, completed, total, hasTodos } = useTodos();

  if (!hasTodos) return null;

  return (
    <aside>
      <div>{completed} / {total}</div>
      <div>{activeTodo?.content}</div>
      {todos.map((todo) => (
        <div key={todo.id ?? todo.content}>{todo.content}</div>
      ))}
    </aside>
  );
}
```

## Full Example

```tsx theme={"dark"}
import {
  OpenHarnessProvider,
  useOpenHarness,
  useSubagentStatus,
  useSessionStatus,
  useTodos,
} from "@openharness/react";

function App() {
  return (
    <OpenHarnessProvider>
      <Chat />
    </OpenHarnessProvider>
  );
}

function Chat() {
  const { messages, sendMessage, status, stop } = useOpenHarness({
    endpoint: "/api/chat",
  });
  const { activeSubagents, hasActiveSubagents } = useSubagentStatus();
  const session = useSessionStatus();

  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>
          {msg.parts.map((part, i) =>
            part.type === "text" ? <span key={i}>{part.text}</span> : null
          )}
        </div>
      ))}
      <button onClick={() => sendMessage({ text: "Hello" })}>Send</button>
    </div>
  );
}
```
