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