@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-only
  • auto
  • full-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.