React hooks
Stream sessions, permissions, plans, and workflows into your UI
@klein-kit/react turns a live agent session into React state — messages, streaming tool output, pending permissions, plans, todos, usage — via useSyncExternalStore, with stable sub-array identities so children memoize cleanly.
pnpm add @klein-kit/react @klein-kit/core reactPeer deps: react >= 18, @klein-kit/core, and optionally @klein-kit/orchestrate (only needed for useWorkflow).
useAgentSession
import { useAgentSession, useProviders } from "@klein-kit/react";
function Chat() {
const { available } = useProviders();
const agent = useAgentSession({ provider: "codex", permissionMode: "ask" });
return (
<>
{agent.messages.map((m) => (
<div key={m.id}>
<b>{m.role}</b>: {m.text}
{m.tools.map((t) => (
<pre key={t.id}>{t.name} [{t.status}] {t.output}</pre>
))}
</div>
))}
{agent.permissions.map((p) => (
<div key={p.id}>
{p.title}
<button onClick={() => agent.respond(p.id, { behavior: "allow" })}>Allow</button>
<button onClick={() => agent.respond(p.id, { behavior: "deny" })}>Deny</button>
</div>
))}
<PromptBox
busy={agent.status === "streaming"}
onSend={(text) => agent.send(text)}
/>
</>
);
}Options are SessionOptions plus:
| Option | Effect |
|---|---|
provider | Required. Changing it starts a new session. |
resume | Resume a persisted nativeSessionId instead of starting fresh. Changing it also restarts. |
enabled | Default true. Set false to defer session creation (e.g. until the user picks a provider). |
onPermissionRequest | Decide in code; omit to surface requests in permissions for your UI to respond() to. |
Other option changes apply to later turns only — re-renders never restart the session. The session is created on mount and closed on unmount.
The result extends SessionSnapshot:
interface SessionSnapshot {
status: "idle" | "streaming" | "closed" | "error";
messages: Message[]; // { id, role, text, streaming, reasoning?, tools: ToolRun[] }
plan: PlanStep[];
todos: TodoItem[];
permissions: PermissionRequest[];
usage: Usage;
limits: RateLimit[];
filesChanged: FileChange[];
diff: string | undefined; // cumulative turn diff, when the provider reports one
queued: { turnId: string; text: string }[];
error: string | undefined;
}plus imperative handles: session, ready, send(input, options?) → turnId, respond(requestId, response), interrupt(), cancelQueued(turnId), and clear() (resets the transcript without touching the session).
ToolRun extends ToolInvocation with status: "running" | "success" | "error" | "canceled" and the streamed output so far — render live tool output directly from the message tree.
useProviders
const { providers, available, loading, error, refresh } = useProviders();
// available = installed && authenticated !== false — the sensible menu contentsWraps detectProviders (side-effect free, safe on mount). Pass useProviders(["codex", "claude"]) to probe a subset.
useWorkflow
Lives on a subpath so @klein-kit/orchestrate stays an optional dependency:
import { useWorkflow } from "@klein-kit/react/workflow";
const wf = useWorkflow();
wf.run(definition); // ignored while a run is in flight
wf.abort(); // interrupts in-flight turns
wf.status; // "idle" | "running" | "completed" | "failed"
wf.steps; // StepView[]: { id, status, provider?, attempt, output, verdict? }
wf.taskState; // updated on every handoff
wf.results; // Record<string, StepResult> when doneEach StepView.output accumulates the assistant text streamed for that step, and verdict reflects reviewer approve/revise decisions — enough to render a live pipeline view with no extra wiring.
SessionStore
The store behind useAgentSession is exported for non-hook contexts (state libraries, tests, SSR shells): getSnapshot(), subscribe(listener), attach(session), detach(), reset().