Skip to content
BeeuniBeeuni
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

React hooks

Run a Beeuni session in React with the useSession hook.

@beeuni/sdk/react adds React hooks on top of the SDK. This page covers useSession, the hook that runs a session end to end, and the other hooks confirmed stable for React apps.

useSession(projectId, sessionId, options?)

useSession starts the session, opens the server-selected event transport, and syncs messages, status, and pending prompts. Call it once per session view.

import { useSession } from '@beeuni/sdk/react';

function Chat({ projectId, sessionId }: { projectId: string; sessionId: string }) {
  const s = useSession(projectId, sessionId);

  if (s.phase !== 'ready') return <Booting stage={s.stage} onRetry={s.retry} />;

  return (
    <>
      {s.messages.map(({ info, parts }) => (
        <Message key={info.id} info={info} parts={parts} />
      ))}
      <Composer busy={s.isBusy} onSend={s.send} onStop={s.cancel} />
    </>
  );
}

Readiness is server truth. The runtime is ready when POST /start returns stage: 'ready'. useSession does not run a separate client-side health check.

Returns

Field Type What it holds
phase 'starting' | 'ready' | 'error' Overall state. Render a boot screen until ready.
messages { info, parts }[] The message list. Parts stream in live.
savedTranscript 'loading' | 'shown' | 'none' Whether the saved conversation can show before the computer wakes. loading: a saved copy is on its way. shown: messages are in the list. none: nothing can show until the runtime answers.
conversationEmpty boolean The saved copy proves the conversation empty, no turn ended since, and nothing is open or queued. Render the composer, not a boot screen.
status SessionStatus The session status.
isBusy boolean The agent is generating a reply.
questions, permissions array Pending agent questions and tool-approval requests. A permission here is one runtime tool approval, not an IAM permission.
diffs, todos array Live file diffs and todo items.
sendError BeeuniSendError | null The last send failure: billing, runtime-not-ready, or runtime-error.
rewindMessageId string | null The selected user message while a reversible rewind is staged.
rewindPending boolean A rewind or restore request is in progress.
rewindError BeeuniSendError | null The last rewind or restore failure.
models, agents, defaultAgent, commands — Selectable models, selectable agents, the default agent, and slash commands. Available before the runtime starts.
retry () => void Force a re-check of /start.

Actions

Action What it does
send(text, override?) Send a prompt. override sets { model?, agent? } for this message only.
sendParts(parts, override?) Send text and runtime URL file parts through the selected transport. Private staged handles use platform prompt routes.
rewind(messageId) Rewind this canonical session to a user message. The selected message and later path become hidden and recoverable.
restoreRewind() Restore the removed path before another prompt commits its replacement.
cancel() Stop the current run and clear pending questions and permissions. Holds the session’s queued prompts first, so a queued prompt does not start a new run.
runCommand(command, args) Run a project slash command.
answerQuestion(id, answers) Answer a pending agent question.
rejectQuestion(id) Reject a pending agent question.
answerPermission(id, reply, message?) Answer a tool-approval request. reply is 'once', 'always', or 'reject'.

useSession also returns removeQuestion and removePermission. Do not use them. They clear the prompt from local state but never notify the agent, so the run stays blocked. Use answerQuestion, rejectQuestion, or answerPermission instead.

Options

Option Default What it does
waitMs 15000 The long-poll budget sent to /start.
replayStartStash true Replay a prompt saved before the session existed, once the session is ready.
enabled true Set false to delay the hook, for example until a billing check passes.
chatEngine true Set false if your app mounts its own chat surface for this session, to avoid syncing messages twice.
initialRuntimeSessionId null A runtime session pin your server already holds for this session. The transcript renders from it while /start runs; the /start answer stays authoritative. initialOpenCodeSessionId is its deprecated name.
browserPresence false Hold this tab’s presence lease while a signed-in person uses the page (input in the last 10 minutes). The lease keeps the session’s computer awake.
presenceAlerts false This tab shows its own notifications. Sent on the presence lease: the server skips the phone and Web Push only while an alerting tab is in use. A change is sent at once. Pass true only when the project’s notification_center flag is on; the server ignores it otherwise. Needs browserPresence.
presencePageExit false End the presence lease when the page closes or enters the back/forward cache, so a turn that ends right after the tab closes still notifies. On, pagehide reports absent, and every absent report is sent with keepalive, so it outlives the page. Off: no pagehide report and no keepalive. A closing tab still turns hidden and reports absent, but the browser can cancel that request as the page unloads; the lease then lives to its 90 s expiry. Pass the project’s notification_center flag. A change applies at once, with no new presence report. Needs browserPresence.
repositoryMode — Telemetry only. A session created before a repository replacement starts and runs the project’s current config without it. Only its /workspace clone still comes from the old repository, so a push from it needs a rebase first.

Sending is optimistic. send shows your message right away, then stream events fill in the agent’s reply.

rewind(messageId) never creates a session. It uses the canonical session from POST /start. The runtime restores file state and keeps the removed transcript path recoverable. The next accepted prompt commits the replacement path.

Other stable hooks

@beeuni/sdk/react also exports React Query hooks for data that does not need a running session. Each mirrors a method on the client and needs no provider.

Hook Reads
useProjectModels(projectId) Selectable models for the project.
useProjectModelPickerCatalog(projectId) The raw /model-picker record per wire model (reasoning_options, temperature, limit) and managedPricingRoutes customer price estimates for each eligible managed route. The serving route determines the final credit charge.
useProjectSessions(projectId, { parent, startedBy, q, limit }) One project’s sessions, paged. parent: 'root' returns top-level sessions with child_count; startedBy: 'me' | 'others' | 'automated' filters by who started the run; q searches every session the viewer may open, not only loaded pages. Each filter set has its own cache entry.
useSessionChildren(projectId, parentSessionId, { q, limit, enabled }) The sessions one parent spawned (parent=<id>), newest first, paged. Set enabled: false until the parent is opened.
useVisibleAgents({ projectId }) The project’s visible agents.
useProjectConfig(projectId) The project’s runtime config: default agent, commands.
useProjectSecrets(projectId) Secrets: list, add, remove, and personal overrides.
useAccountSecretResources(accountId) Account provider-secret metadata and use grants. Values are never returned.
useSessionProviderSecretPools(projectId, sessionId) Configured pools, including empty selections; setPool updates one provider and refreshes the session cache.
useProjectTriggers(projectId) Triggers: list, create, update, remove, fire.
useProjectTriggerEventTypes(projectId, connector) The app events a connector can trigger on. Idle until connector is set.
useChangeRequests(projectId, status?) Change requests: list, open, merge, close, request changes.
useRuntimeSupports(capability) Whether the open session’s runtime serves a feature (session.rewind, session.compact, …), from the /beeuni/health answers the runtime-reconnect poller reads. true until the first answer. See What the runtime supports.
useNotificationInbox({ userId, limit?, enabled? }) The caller’s newest inbox page (limit default 20), unreadCount, and markRead(ids), markAllRead(), markSessionRead(sessionId). Polls every 60 s while the page is visible and refetches on focus. A write changes the cache at once, restores it on failure, then refetches every inbox entry. No request without userId.
useNotificationPreferences({ userId, enabled? }) The caller’s push and email choice per kind, email_available, and update(patch) (optimistic).
useSessionWatch({ userId, projectId, sessionId, enabled? }) { watching } for one session, and setWatching(watching) (optimistic) to mute or watch it.
resetRuntimeQueries(queryClient) Not a hook. Drops every cached runtime query (sessions, messages, agents, commands, diffs). Call it after the session’s runtime is replaced by a restart or a config reload, so the next read comes from the new runtime.

The three notification hooks key their cache by userId (qk.notifications), so a user switch in the same tab never shows the previous user’s inbox. Pass the signed-in user’s id.

Notifications are behind the per-project notification_center feature flag, off by default. Gate each hook with enabled: useFeatureFlag(projectId, 'notification_center').enabled, so a project with the flag off sends no notification request.

An agent can mint a 1-click connect link (/connect/<token>). The flow’s data and runtime policy live in the SDK; a host renders its own dialog over them and injects its adapters.

Export Role
useConnectorSetup(token, options) The whole flow: load what the link names, POST /start, open the provider’s hosted page through the injected openPopup, poll POST /finalize on the 3 s / 5 s / 5-minute schedule, and report phase (loading, ready, starting, opened, connected, error), the app info, the error, and who the account was authorized as. options carries the host’s backendUrl, storage adapter and openPopup; onOpened fires after each popup opening.
useConnectorLinkInfo(token, options) One shared, storage-backed cache per link token, so every card and the modal share one GET of the rate-limited public route and paint the app’s logo on the first frame. The same backendUrl/storage options.
createConnectorLinkInfoCache(fetchInfo, { storage }) The cache factory itself (framework-free, storage injected) for a host that wants its own instance.
resolveConnectorStart({ start, finalize }) The /start outcome rule: hosted url → popup, already connected → finalize once, otherwise the error message.
nextConnectorPollDelay(attempt, elapsedMs) The bounded poll schedule (3 s first, 5 s after, stops at 5 minutes).

Connector data

useConnectorQuery(projectId, slug, action, args, { account, enabled, staleTime }) reads one connector action’s output as a cached query. error is a ConnectorCallError: render a connect button from error.connectUrl, or an account picker from error.availableAccounts. Use it only for an action that reads; every fetch runs the action and writes an audit event. The output stays fresh for 30 s, focus never refetches, and only an upstream 429 or 503 is retried. See Connectors from code.

Start the open read early

prefetchSessionOpen(queryClient, projectId, sessionId) starts the session-open read before the session view mounts. It issues one GET .../snapshot request (session row, turn, queue, and the first 40 transcript messages) and seeds the session row cache from it. useSession then uses that request instead of starting its own.

Call it when a route names a session, or on hover, focus, or touch of a session link. It is read-only: it never calls /start and never wakes a computer. It never rejects. A repeat call for the same session within 30 seconds sends no request.

import { prefetchSessionOpen } from '@beeuni/sdk/react';

<a
  href={`/projects/${projectId}/sessions/${sessionId}`}
  onFocus={() => void prefetchSessionOpen(queryClient, projectId, sessionId)}
/>;

Render from cache on a reload

Two stores keep what a user already saw, so a reload or a cold app start renders it in the first frame and refreshes it in place. Both are per user and bounded, and a host clears both on sign-out.

  • createPersistedQueryCache keeps the React Query results a user navigates by: accounts, projects, project detail, the paged session list, and session rows (isPersistableQueryKey). A restored entry keeps its original age, so it refetches on mount. The default bounds are 1,000,000 characters and 7 days.
  • createSavedCopyStore keeps the last saved copies of transcripts that the server sent. Register it with setSavedCopyStore, and useSession paints the kept copy before the first frame. The server’s copy then reconciles into it by message ID, and the SDK re-reads it 3 seconds after each turn ends. The default bounds are 12 sessions, 1,500,000 characters, and 14 days. A copy over 300,000 characters keeps its newest messages that fit; older messages load from the server when the user scrolls up.
import {
  browserKeyValueStorage,
  createPersistedQueryCache,
  createSavedCopyStore,
  isPersistableQueryKey,
  setSavedCopyStore,
} from '@beeuni/sdk';

const storage = browserKeyValueStorage(); // null where there is no localStorage
if (storage) {
  const cache = createPersistedQueryCache({ storage, userId, shouldPersist: isPersistableQueryKey });
  cache.restore(queryClient); // synchronous with localStorage: no pending frame
  const stop = cache.persist(queryClient);
  setSavedCopyStore(createSavedCopyStore({ storage, userId }));
}

On React Native, pass AsyncStorage as storage. restore then returns a promise.

The saved-copy store keeps only server captures: the transcript mirror that the API writes when a turn ends. The live transcript is never written to the device, so a stopped turn cannot come back as a running one. A runtime read that returns no messages keeps the saved messages on screen: it means the computer lost its state, not that the conversation is empty. The first runtime read with messages reconciles them.

Sign-out and account switching

The SDK keeps per-user session state in memory: transcripts, pending questions and permissions, turn receipts, and model picks. Call resetIdentityState() on every identity change: sign-out, and a different user signing in. This includes a sign-in in another tab that changes the user without a page load.

Call this handler when your authentication flow signs a user in or out.

import { resetIdentityState } from '@beeuni/sdk/react';

let previousUserId: string | null = null;

function onIdentityChange(userId: string | null) {
  if (userId !== previousUserId) {
    queryClient.clear();
    resetIdentityState();
  }
  previousUserId = userId;
}

resetIdentityState() also removes the model store’s localStorage entry. It does not clear the host’s own caches, such as React Query, or the token source passed to createBeeuni. Reset those in the same handler. It never throws.

With the stores from Render from cache on a reload, clear them in the same handler: await cache.clear(), then await savedCopies.clear() and setSavedCopyStore(null). Neither store ever restores data for a different user, but a signed-out device should not keep it.

Next

  • Sessions — the session handle useSession wraps, and the BeeuniSendError kinds.
  • Reference — the full REST surface these hooks read from.

Saved history during startup

useSession(projectId, sessionId) shows saved chat history while its computer starts. The SDK reads the database independently of runtime readiness. Live messages then reconcile by their original IDs. Hosts do not need a separate history request or message store.

Saved history renders exactly like the live transcript: text, reasoning, and every tool call with its input, output and error. A sub-agent’s full view reads the sub-agent’s own saved transcript: pass the parent’s scope and savedChild: true to useSessionSync(subAgentSessionId, { beeuniSessionScope, savedChild: true }). The initial window contains the newest 40 messages, at most 1,000,000 characters. Scrolling up loads older windows from saved history, also while the computer is off. Attachments that require the computer become available when the runtime is ready.

savedTranscript tells a host what to render while the computer starts. It is loading while a saved copy may still arrive, shown once messages are in messages, and none when nothing can show before the runtime answers. An unknown is loading, never none.

conversationEmpty is true when there is nothing to wait for. The saved copy proves the conversation empty: a complete read of the session’s runtime found no messages. No turn ended since, and nothing is open or queued. Missing records never make it true, so an older session with history is never shown as empty. A session gets that proof at its first start with saved history on.

if (s.messages.length === 0) {
  // Nothing saved and nothing to wait for: the user can type now.
  if (s.conversationEmpty) return <Composer />;
  // One round trip for a saved copy; the computer can take minutes.
  return s.savedTranscript === 'none' ? <Booting stage={s.stage} /> : <MessageSkeleton />;
}

Tool calls saved before 2026-09-28 lost their input and output. The server serves each one with the input its kept title or metadata proves: a command’s command and output, a file call’s path, a search’s pattern, a sub-agent’s description, a fetch’s URL, a todo list, a skill’s name. The session’s next start saves the whole history again, 1.

Pending prompts

useSessionPrompts(projectId, sessionId) submits to the durable session inbox. The server runs pending prompts automatically, each as its own turn.

Pass placement: 'transcript' to display a pending message in the conversation. It runs before every composer entry and ends the active response after the current tool call completes. Pass placement: 'composer' to keep it in the editable list above the input. It waits for the active response to finish. Each placement keeps submission order. Pass delivery: 'steer' | 'queue' | 'interrupt' to say how the prompt reaches a running turn. steer hands it to the turn at its next step; the turn does not stop. queue waits for the turn to end. interrupt ends the turn after the running tool. With only delivery, the SDK sends the placement it implies: interrupt is transcript, the others are composer. A steer row that cannot steer reads delivery: 'queue' with steer_fallback set to unsupported, not_prompter or turn_ended. interrupt(promptId) (“Stop and send”) turns a waiting row into a transcript entry; a row already on the wire answers 409.

The web composer sends Enter as steer while a turn runs, and as a transcript send while the session is idle. Command+Enter (Control+Enter on Windows/Linux) is composer placement. Shift+Enter inserts a newline.

List rows include full_text for complete previews after reload. text remains the bounded preview for older clients. Attachment bytes are excluded from list responses. Delete returns the complete prompt for lossless undo.

Pending delivery and active responses

useSessionWorking() keeps state: 'working' while a prompt waits in the durable inbox. This keeps Stop available. pendingDelivery: true marks a prompt still on its way to the runtime; the web app shows one working indicator whenever state is working, whatever that flag says. Active runtime turns and fresh runtime activity clear it. A timed-out cancel does not confirm that execution stopped.

A worker claim only checks admission and keeps the prompt waiting. Delivery starts after admission succeeds. A confirmed active turn clears the pending presentation even if the previous inbox snapshot still lists that prompt. Runtime activity preserves the active turn’s message ID during this handoff.

The web composer names Command/Ctrl+Enter Queue List and a row’s “Stop and send” Quick Queue. Quick Queue entries run first. A waiting Quick Queue entry is a tinted bubble after delivered turns, with no status text. Queue List entries remain editable until delivery starts. The server recovers missed completion events from exact runtime evidence, including after reload, without treating an unreachable runtime as a completed turn.

Each distinct Enter displays its pending prompt immediately, even while an earlier acceptance request is unresolved. Only a delivery failure adds text: its cause, Retry, and Remove. The server inbox continues to own execution order.

The hook reads queue changes from the session’s control stream (GET .../sessions/:id/events?channels=control), one connection per session however many components mount it. Every write of an inbox row arrives as a beeuni.control.queue frame. The GET .../prompts poll runs only while that stream is not connected.

Why a turn ended

The transcript of an interrupted turn only carries MessageAbortedError. The control plane keeps what it knows about how each turn ended: the cause the sandbox named, for example SandboxMemoryGuard, and which turns died with no cause named. A stop somebody asked for — the Stop button, abort(), a prompt sent into a busy session — is recorded as a request and is never reported as a failure.

useSessionTurnOutcome(projectId, sessionId) returns last_ended, recent_failures and the read time atMs from the cache useSessionWorking() keeps fresh. It makes no request. Pass the result to turnEndNotice, with what the transcript says about the turn:

const outcome = useSessionTurnOutcome(projectId, sessionId);
const notice = turnEndNotice(outcome, turn.userMessage.info.id, {
  hasError: Boolean(turnError),
  isAbort: turnErrorIsAbort,
});
// notice: { kind: 'sandbox-memory', usedPct: 97, detail: 'sandbox memory at 97% …' }
//       | { kind: 'cause', name: 'SomeGuard', message: '…' }
//       | { kind: 'unexplained' }
//       | null

The host maps each kind to its own copy and never parses the sandbox’s message. null means “show what the transcript shows”: a real error stays, an abort renders nothing. An unexplained failure is reported only once the reading is a few seconds past the turn’s end, because the cause often arrives one frame after the abort. turnEndCause(outcome, messageId) returns the raw recorded cause, or null.

Was this page helpful?