This skill should be used when the user asks to "connect React to agent runtime", "use useAgentSession", "use useMessages", "set up AgentServiceProvider", "stream agent responses", "build agent chat UI", "render conversation blocks", or needs to build a React frontend with @hhopkins/agent-runtime-react.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
references/types.mdThe @hhopkins/agent-runtime-react package provides React hooks and context for connecting to the agent runtime backend. It handles:
pnpm add @hhopkins/agent-runtime-react
Wrap the application with AgentServiceProvider:
import { AgentServiceProvider } from "@hhopkins/agent-runtime-react";
function App() {
return (
<AgentServiceProvider
apiUrl="http://localhost:3001" // REST API URL
wsUrl="http://localhost:3001" // WebSocket URL (same server)
apiKey="your-api-key" // API key for auth
debug={false} // Enable debug logging
>
<YourApp />
</AgentServiceProvider>
);
}
The provider:
Manage session lifecycle - create, load, destroy sessions:
import { useAgentSession } from "@hhopkins/agent-runtime-react";
function SessionManager() {
const {
session, // Current session state (null if not loaded)
runtime, // Runtime state (sandbox status)
isLoading, // Operation in progress
error, // Last error
createSession, // Create new session
loadSession, // Load existing session
destroySession, // Destroy current session
syncSession, // Manually sync to persistence
updateSessionOptions, // Update session options
} = useAgentSession(sessionId); // Optional: auto-load on mount
// Create a new session
const handleCreate = async () => {
const newSessionId = await createSession(
"agent-profile-id", // Agent profile reference
"claude-agent-sdk", // Architecture type
{ model: "sonnet" } // Optional session options
);
};
return (
<div>
<p>Sandbox: {runtime?.sandbox.status}</p>
<button onClick={handleCreate}>New Session</button>
</div>
);
}
Important: Call useAgentSession at the page/container level to ensure WebSocket room is joined regardless of which child components render.
Access conversation blocks and send messages:
import { useMessages } from "@hhopkins/agent-runtime-react";
function Chat({ sessionId }: { sessionId: string }) {
const {
blocks, // ConversationBlock[] - pre-merged with streaming
streamingBlockIds, // Set<string> - IDs currently streaming
isStreaming, // boolean - any block streaming
metadata, // Token/cost info
error, // Last error
sendMessage, // Send message to agent
getBlock, // Get block by ID
getBlocksByType, // Filter blocks by type
} = useMessages(sessionId);
const handleSend = async (text: string) => {
await sendMessage(text);
// Response arrives via WebSocket events
};
return (
<div>
{blocks.map((block) => (
<BlockRenderer
key={block.id}
block={block}
isStreaming={streamingBlockIds.has(block.id)}
/>
))}
<MessageInput onSend={handleSend} disabled={isStreaming} />
</div>
);
}
Streaming behavior: Blocks are pre-merged with streaming content. A temporary block with ID "streaming" appears during active streaming.
List all sessions:
import { useSessionList } from "@hhopkins/agent-runtime-react";
function SessionList({ onSelect }: { onSelect: (id: string) => void }) {
const { sessions, isLoading, error, refresh } = useSessionList();
return (
<ul>
{sessions.map((session) => (
<li key={session.sessionId} onClick={() => onSelect(session.sessionId)}>
{session.sessionId} - {session.type}
</li>
))}
</ul>
);
}
Track files modified by the agent:
import { useWorkspaceFiles } from "@hhopkins/agent-runtime-react";
function FileExplorer({ sessionId }: { sessionId: string }) {
const { files, getFile } = useWorkspaceFiles(sessionId);
return (
<ul>
{files.map((file) => (
<li key={file.path}>
{file.path}
<pre>{file.content}</pre>
</li>
))}
</ul>
);
}
Track subagent transcripts:
import { useSubagents } from "@hhopkins/agent-runtime-react";
function SubagentViewer({ sessionId }: { sessionId: string }) {
const { subagents, getSubagent } = useSubagents(sessionId);
return (
<div>
{subagents.map((subagent) => (
<div key={subagent.id}>
<h4>{subagent.name}</h4>
<p>Status: {subagent.status}</p>
</div>
))}
</div>
);
}
Access debug event log for monitoring WebSocket events:
import { useEvents } from "@hhopkins/agent-runtime-react";
function DebugPanel() {
const { events, clearEvents } = useEvents();
return (
<div>
<button onClick={clearEvents}>Clear</button>
{events.map((event, i) => (
<pre key={i}>{JSON.stringify(event, null, 2)}</pre>
))}
</div>
);
}
Handle different block types when rendering:
function BlockRenderer({ block, isStreaming }) {
switch (block.type) {
case "user_message":
return <UserMessage content={block.content} />;
case "assistant_text":
return (
<AssistantMessage
content={block.content}
isStreaming={isStreaming}
/>
);
case "tool_use":
return (
<ToolCall
name={block.toolName}
input={block.input}
/>
);
case "tool_result":
return (
<ToolResult
content={block.content}
isError={block.isError}
/>
);
case "thinking":
return <ThinkingBlock content={block.content} />;
case "subagent":
return (
<SubagentCall
name={block.name}
status={block.status}
/>
);
case "error":
return <ErrorMessage message={block.message} />;
default:
return null;
}
}
function Chat({ sessionId }) {
const { blocks, isStreaming } = useMessages(sessionId);
return (
<div>
{blocks.map((block) => <BlockRenderer key={block.id} block={block} />)}
{isStreaming && <TypingIndicator />}
</div>
);
}
function AssistantMessage({ content, isStreaming }) {
return (
<div className={isStreaming ? "streaming" : ""}>
{content}
{isStreaming && <span className="cursor">|</span>}
</div>
);
}
User messages appear immediately via optimistic updates. The hook dispatches OPTIMISTIC_USER_MESSAGE, then replaces it with the real message when block_complete arrives.
Errors are surfaced in multiple ways:
function Chat({ sessionId }) {
const { error: sessionError } = useAgentSession(sessionId);
const { error: messageError, blocks } = useMessages(sessionId);
// Check hook-level errors
if (sessionError) return <Error message={sessionError.message} />;
// ErrorBlocks appear inline in the conversation
const errorBlocks = blocks.filter((b) => b.type === "error");
return <div>...</div>;
}