@connectonion/react
September 7, 2026 ยท View on GitHub
React hooks and browser primitives for ConnectOnion agents over OIP.
The package owns the browser connection end to end: OIP endpoint discovery,
the authenticated /ws session, browser identity, reconnect and onboarding,
mode acknowledgements, event normalization, and the Zustand-backed
React session store. It has no dependency on the retired connectonion-ts
package and uses OIP exclusively.
Install
npm install @connectonion/react
React 17 or newer is required as a peer dependency.
React hook
import { useAgentForHuman } from '@connectonion/react'
export function AgentChat({ address }: { address: string }) {
const agent = useAgentForHuman(address)
return (
<form onSubmit={(event) => {
event.preventDefault()
const data = new FormData(event.currentTarget)
agent.input(String(data.get('message') ?? ''))
}}>
{agent.ui.map((item) => (
<pre key={item.id}>{JSON.stringify(item, null, 2)}</pre>
))}
<input name="message" />
<button disabled={agent.isProcessing}>Send</button>
</form>
)
}
useAgentForHuman exposes the conversation ui, connection and processing
state, the current Todo List, authenticated agent profile and dashboard, plus
actions for prompts, onboarding, approvals, interruption, reconnect, and
mode changes.
Onboarding pauses the original CONNECT attempt. Submit the signed invite or
payment assertion through signOnboard and sendMessage; after verification,
the Host completes that same connection so the original prompt is not run twice.
Low-level connection
import { RemoteAgent } from '@connectonion/react/connect'
const agent = new RemoteAgent('0x...')
const result = await agent.input('Summarize this repository')
console.log(result.text)
By default the client discovers a browser-reachable OIP endpoint through the OpenOnion relay and falls back to the relay OIP socket when HTTPS mixed-content rules make a local endpoint unreachable. A trusted direct deployment can be selected explicitly:
const agent = new RemoteAgent('0x...', {
directUrl: 'https://my-agent.example',
})
When a host advertises its protocol in CONNECTED, the client accepts OIP 0.1
and reports a clear error for an unsupported protocol or version.
Retained session synchronization
Hosts that select the experimental session-sync/0.1 extension expose the
authenticated identity's retained conversations through typed RemoteAgent
methods:
await agent.connect()
const index = await agent.syncSessions({ cursor: previousCursor })
const snapshot = await agent.getSession(index.sessions[0].session_id)
await agent.updateSession(index.sessions[0].session_id, { archived: true }, 7)
For a sidebar/index worker, construct RemoteAgent with
{ sessionSyncOnly: true }. That CONNECT authenticates and negotiates the
extension without creating an empty conversation on Host.
Every CONNECT includes a fresh signed nonce, so simultaneous pages from the
same identity remain distinct under Host replay protection.
syncSessions() and getSession() drain protocol pagination before resolving.
Opaque cursors and Host revisions must be persisted unchanged. All Session Sync
commands are individually signed, even during compatibility with older OIP
application frames. Host-retained committed history is authoritative; browser
storage remains the cache, draft, and unsent-outbox boundary.
Modes
The Host exposes exactly three public modes:
read-onlyautofull-access
Read mode, turnsLeft, and availableModes, then await
setSessionMode(). The client sends an OIP mode_change and updates local
authority only after the matching mode_changed acknowledgement. Every fresh
or unknown stored state becomes Auto; old spellings are not translated into
authority. Full access always carries a positive bounded turnsLeft value.
Codex and Claude Code child activity arrives as ordinary OIP
provider_invocation, tool_call, and tool_result events. The package nests
that activity under one provider card without requiring another transport.
Provider-native Work Room permissions
A valid provider_invocation.providerPermission exposes the finite Codex or
Claude Code catalog authored by Host for that exact positive stateRevision.
The nested state is separate from outer COAI mode and individual approvals.
Malformed, stale, mismatched, oversized, or unknown catalogs are discarded.
Call setProviderPermission(invocationId, optionId, confirmRisk?) on
RemoteAgent or the object returned by useAgentForHuman. The SDK sends a
signed, revision-bound request and does not update local authority
optimistically. It resolves only after a matching accepted Host ACK contains a
strictly newer valid state with the requested active option. Elevated native
profiles require the caller to pass the explicit confirmation flag.
Browser identity
initializeBrowserIdentity() stores a non-extractable Ed25519 key in IndexedDB.
createBrowserIdentity(), importBrowserIdentity(), and
claimPendingBrowserRecovery() cover explicit replacement and recovery flows.
The raw in-memory helpers generateBrowser, signBrowser, and
createSignedPayloadBrowser never persist keys.
Development
npm ci
npm run typecheck
npm test
npm run build
Releases are created only by the protected GitHub tag workflow. A tag must match
package.json and point at the current reviewed main commit; npm publishing
uses Trusted Publishing with provenance.
Control Center browser bridge
A reviewed static app uses the existing parent SDK connection. It does not create
an Agent connection or receive browser keys. Framework bundles can import
connectControlCenter from @connectonion/react/control-center/browser; vanilla
apps can copy the built browser entry point and its license into their output.
const client = await connectControlCenter({parentOrigin, revision});
client.subscribe(({chatItems, status, connectionState, truncated}) => {
// Render the parent's normalized conversation and connection state.
});
await client.sendMessage('Explain this invoice');
await client.runSkill('generate-invoice', 'invoice 1042', {signal});
The O Chat shell supplies parentOrigin and revision in the app URL fragment,
verifies the iframe's origin/window and transfers one MessagePort per load. A fresh
epoch scopes ordered snapshots and correlated actions; gaps request a new snapshot.
boundControlSnapshot preserves recent complete ChatItems within a byte limit and
marks omitted history. Actions default to the current conversation; only an explicit
conversation: 'new' requests another one. Requests accept AbortSignal and bounded
timeouts. Cancellation/disposal reaches only a pending owned action. Current-chat
turns must start while the Agent is idle and await completion; new-chat navigation
acknowledges the handoff. A bare static URL has no implicit identity/session.
The parent uses createControlCenterHost with its normalized snapshot and callbacks,
then calls publish as its SDK state changes. useAgentForHuman exposes
controlCenterState, controlCenterCommand, and inputFromControlCenter. The latter
refuses busy Agents and supports cancellation before/after dispatch. Control commands
(state/update/configure/source/diff/rollback) use signed, correlated frames over the
existing direct or Relay transport; author operations require Host administrator
access. Reset clears review/active state. allowLocalhost: true is an explicit
local-development option for an HTTP loopback parent; production requires HTTPS.