@picoagent/embed-reactwraps the core SDK in React hooks. Session lifecycle, plan state, agent status, glanceable text, and credits each have a dedicated hook that handles subscription and cleanup automatically.
Using the React SDK
Installation
npm install @picoagent/embed-react @picoagent/embed-voice
@picoagent/embed-voice is listed separately because it bundles the LiveKit audio transport, which has its own native dependencies. Install both for any session that requires live voice.
Available Hooks
| Hook | Package | Returns | When to use |
|---|---|---|---|
useSession | @picoagent/embed-react | { session, status, error } | Always — manages the session lifecycle. |
usePlan | @picoagent/embed-react | { currentStep, currentStepIndex, totalSteps } | When your UI shows a step panel or progress indicator. |
useAgentStatus | @picoagent/embed-react | string | When you want to show what the agent is currently doing. |
useGlanceable | @picoagent/embed-react | string | null | When you need a persistent HUD banner or AR overlay text. |
useCredits | @picoagent/embed-react | { creditsUsed, sessionMinutes, ratePerMin } | null | When you want a usage meter visible to the worker. |
useSession(credential)
Manages the full session lifecycle. Pass it the credential object fetched from your backend token endpoint.
const { session, status, error } = useSession(credential);
status values:
| Value | Meaning |
|---|---|
"idle" | No credential yet, or the hook has not started connecting. |
"connecting" | createSession() has been called; waiting for the LiveKit room to open. |
"connected" | Session is live and ready to accept transport attachments and event subscriptions. |
"error" | A fatal error occurred. Inspect error.message and surface it to the user. |
Pass session to all other hooks. They are all null-safe — they return empty/null state when session is null, so you do not need to guard each hook call yourself.
usePlan(session)
Returns the current plan state. Updates automatically as plan and step_update events arrive.
const plan = usePlan(session);
// plan.currentStep — active step object (null before first plan)
// plan.currentStepIndex — zero-based index
// plan.totalSteps — total step count
When session is null, usePlan returns { currentStep: null, currentStepIndex: 0, totalSteps: 0 }.
useAgentStatus(session)
Returns a string describing what the agent is currently doing — useful for a status label or animated indicator.
const agentStatus = useAgentStatus(session);
// e.g. "listening", "thinking", "speaking"
Common values: "listening", "thinking", "speaking". The exact set depends on the agent configuration.
useGlanceable(session)
Returns the latest glanceable text string emitted by the agent, or null if none has arrived yet. Updates in near-real time as the agent produces observations.
const glanceable = useGlanceable(session);
// e.g. "Watch your elbow angle" or null
Render it conditionally — null means there is nothing current to display:
{glanceable && <div className="glance-banner">{glanceable}</div>}
useCredits(session)
Returns the current credit usage snapshot, or null before the first update arrives.
const credits = useCredits(session);
// credits.creditsUsed — total credits consumed
// credits.sessionMinutes — elapsed time in minutes
// credits.ratePerMin — burn rate per minute
Full Widget Example
The example below combines all five hooks into a single GuidedTaskWidget component. It also shows how to attach the voice transport inside useEffect for proper cleanup.
import { useSession, usePlan, useAgentStatus, useGlanceable, useCredits } from "@picoagent/embed-react";
import { attachVoice } from "@picoagent/embed-voice";
import { useEffect, useRef, useState } from "react";
function GuidedTaskWidget({ credential }) {
const { session, status, error } = useSession(credential);
const plan = usePlan(session);
const agentStatus = useAgentStatus(session);
const glanceable = useGlanceable(session);
const credits = useCredits(session);
const voiceRef = useRef(null);
const [agentSpeaking, setAgentSpeaking] = useState(false);
useEffect(() => {
if (!session) return;
voiceRef.current = attachVoice(session, {
onAgentSpeaking: (s) => setAgentSpeaking(s),
});
return () => voiceRef.current?.disconnect();
}, [session]);
if (status === "connecting") return <p>Connecting...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<p>Agent: {agentStatus}</p>
{glanceable && <div className="glance-banner">{glanceable}</div>}
{plan.currentStep && (
<div>
<h3>Step {plan.currentStepIndex + 1} of {plan.totalSteps}</h3>
<p>{plan.currentStep.description}</p>
<button onClick={() => session?.send({ type: "command", cmd: "next_step" })}>
Next
</button>
</div>
)}
{credits && (
<div className="credits-hud">
{credits.creditsUsed} credits used ({credits.sessionMinutes.toFixed(1)} min)
</div>
)}
</div>
);
}
Key points in the example
attachVoiceis called insideuseEffectwithsessionas the dependency. This ensures the voice transport is attached as soon as the session is live and disconnected when the component unmounts.session?.send(...)uses optional chaining — safe to call even whilesessionis null during initial render.credits.sessionMinutes.toFixed(1)formats elapsed time to one decimal place.- The
agentSpeakingstate wired fromonAgentSpeakingis available if you want to animate a speaking indicator (e.g. pulsing avatar).
Next Steps
- Plan State, Glanceable, Knowledge, Credits — The underlying vanilla JS APIs that these hooks wrap
- Voice and Video Transports —
attachVoiceandattachVideoin depth