Skip to main content
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

npm install @openharness/react
Wrap your app (or chat component) with OpenHarnessProvider:
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:
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:
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.
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.

useSubagentStatus

Derives reactive state from data-oh:subagent.* events:
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:
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:
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

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>
  );
}