npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

omnix-chat

v3.0.6

Published

Embeddable Agent chat SDK for the browser. Configure with a DSN and drop a chat widget into any web page.

Readme

omnix-chat

Embeddable Agent chat SDK for the browser. Configure with a DSN, drop a chat widget into any web page, and let your end users talk to your agent backend.

  • 🪶 Headless core — omnix-chat is a small client; the full UI ships in omnix-chat/react with antd 6 + @ant-design/x bundled in (host does not need to install or upgrade antd).
  • 🔌 React only — install react + react-dom as peers; no antd 6, no @ant-design/x in your app.
  • 🛡 Host isolation — light-DOM shell with .ac-embedded-host-scoped static CSS, antd css-in-js confined to .ac-embedded-mount, and overlays in .ac-popup-layer (safe alongside antd 4 or other UI libraries).
  • 📦 ES2020 default — omnix-chat/react is post-built to ES2020; webpack 5 / Vite hosts need no extraBabelIncludes / extraBabelPlugins for the package.
  • 🧩 Legacy entry — omnix-chat/react/legacy (ES2018) for Umi 3 / nodeModulesTransform: 'none'.
  • 🖥 Panel layouts — sidebar, floating, or fullscreen; compact session rail with expand/collapse; macOS-style window controls in floating mode.
  • 📍 Page context — route info always sent; optional extended pageContext with preview in the input footer; @ mentions to attach selected context per message.
  • ✍️ Write confirmation — destructive writes pause on an inline card on the assistant bubble (no fullscreen modal).
  • 🔧 Host tools — useAppAgent + usePageAgent for app/page tool declaration, execution, and DSL lifecycle; SSE host_action runs registered handlers.
  • 📋 List PageAction — usePageActions for per-row invoke state on tables (v2.2.2+).
  • 📝 Host Markdown — <Markdown /> from omnix-chat/react (GFM + DOMPurify; no extra md package); approval / task-center previews use the same pipeline (v2.2.5+).
  • ⚡ Lean business-page hooks — usePageActions / usePageAgent / useAppAgent avoid subscribing to full chat state; use useAgentChatClient + useAgentChatStore when you need a slice only (v2.2.5+).
  • ⏳ Login suspend — all methods requiring login automatically wait when the user is not logged in, then resume after login (v2.2.9+).
  • 📎 Apply to page — task-center navigates via routePath + pendingApply; usePageAction({ ready }) auto-replays when the page is ready (v2.3.3+).

Status: public beta. API surface is stable; backend HTTP contract may evolve. Current version: 3.0.5

Which entry should I use?

| Host stack | Import | Babel on node_modules | |------------|--------|-------------------------| | Vite / webpack 5 (modern) | omnix-chat/react | Not required | | Umi 3 / old Babel | omnix-chat/react/legacy | Not required |

Both entries bundle the same UI (antd 6 inside the package). Only syntax level differs.

Quick start

中文用法指南:docs/用法指南.zh-CN.md · Umi 改造:docs/umi-integration.zh-CN.md

CDN drop-in (<script> tag)

Only React is required besides the SDK — antd and @ant-design/x are bundled inside omnix-chat/react.

<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/omnix-chat@latest/dist/react.umd.js"></script>
<script>
  // Use the named export from the UMD build (see dist types for full API).
  const { AgentChat } = OmnixChatReact;
</script>

For the headless client (AgentChat.init without pre-built UI):

<script src="https://unpkg.com/omnix-chat@latest/dist/omnix-chat.umd.js"></script>
<script>
  OmnixChat.init({ dsn: 'your-app-dsn', baseUrl: 'https://api.example.com' });
</script>

npm + React(推荐,开箱即用)

无需在宿主项目安装 antd 6 或 @ant-design/x — 它们已内置在 omnix-chat/react 中。

pnpm add omnix-chat react react-dom
import { AgentChat } from 'omnix-chat/react';

export function App() {
  return (
    <AgentChat
      dsn="your-app-dsn"
      baseUrl="https://api.example.com"
      accountToken="optional-host-token"
      project={{ name: 'My Assistant', logo: '/logo.png' }}
      locale="zh-CN"
    />
  );
}

Umi 3 / webpack 5(推荐 legacy 入口)

Umi 3 请使用 omnix-chat/react/legacy,并关闭对 node_modules 的 Babel 转译(否则 会把包内预编译产物再跑一遍 Babel,出现 @babel/runtime/regenerator 等解析错误):

pnpm add omnix-chat react react-dom
import { AgentChat } from 'omnix-chat/react/legacy';
// config/config.ts 或 .umirc.ts
export default {
  nodeModulesTransform: {
    type: 'none',
  },
  // 若仍走 MFSU 预构建,可排除 omnix-chat:
  // mfsu: { exclude: ['omnix-chat'] },
};

react / react-dom 为必装 peer。聊天 UI(antd)在 async chunk 中按需加载; 请保持 nodeModulesTransform: { type: 'none' },以便 webpack 能解析 node_modules/omnix-chat/dist/react.legacy-*.js。自组 Provider + 面板时:

import { AgentChatProvider } from 'omnix-chat/react/legacy';
import { AgentChatEmbedded } from 'omnix-chat/react/legacy/ui';

开箱即用仍推荐只写:

import { AgentChat } from 'omnix-chat/react/legacy';

npm + vanilla JavaScript(无 UI)

pnpm add omnix-chat
import { AgentChat } from 'omnix-chat';

AgentChat.init({
  dsn: 'your-app-dsn',
  baseUrl: 'https://api.example.com',
  locale: 'zh-CN',
});

React(自定义布局)

import { AgentChatProvider, useAgentChat } from 'omnix-chat/react';
import { AgentChatEmbedded } from 'omnix-chat/react/ui';


export function App() {
  return (
    <AgentChatProvider dsn="your-app-dsn" baseUrl="https://api.example.com">
      <YourApp />
      <ChatShell />
    </AgentChatProvider>
  );
}

function ChatShell() {
  const { instance, isOpen, open, close } = useAgentChat();
  return (
    <>
      <button type="button" onClick={isOpen ? close : open}>
        {isOpen ? 'Hide chat' : 'Need help?'}
      </button>
      <AgentChatEmbedded client={instance} />
    </>
  );
}

DSN format

A DSN is a URL-shaped string that encodes everything the SDK needs to talk to your backend:

https://<publicKey>@<host>[:<port>][/<basePath>]/<projectId>
  • publicKey — the project's public key. Visible on the client; never use it to sign sensitive operations.
  • host/port/basePath — origin and base path of your agent API. The SDK computes apiBase = ${host}:${port}${basePath}/v1.
  • projectId — string or numeric project identifier.

Examples:

https://[email protected]/42
https://[email protected]:8443/api/edge/proj-7
agent://[email protected]/9       # alias, normalized to https://

Parsing / normalization:

import { normalizeDsn, DEFAULT_BASE_URL } from 'omnix-chat';

const dsn = normalizeDsn('your-app-dsn');
await AgentChat.init({ dsn, baseUrl: DEFAULT_BASE_URL });

Configuration reference

| Option | Type | Default | Description | | --- | --- | --- | --- | | dsn | string | — | Required. See DSN format. | | user | UserContext | — | End-user identity. Forwarded to the backend at handshake time. | | locale | string | navigator.language | BCP-47 locale (en, zh-CN, …). Drives both SDK and antd copy. | | theme | 'light' \| 'dark' \| 'auto' | 'auto' | Visual theme. auto follows prefers-color-scheme. | | themeToken | Record<string, unknown> | — | antd theme token overrides (e.g. { colorPrimary: '#7c3aed' }). | | position | 'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left' | 'bottom-right' | Where the floating launcher anchors. | | launcherMode | 'compact' \| 'fab' \| 'hidden' | 'compact' | Idle edge-peek launcher; expands on hover / pending / streaming. fab = always full ball; hidden = no built-in launcher. | | container | HTMLElement \| string | document.body | Where the widget host (.ac-embedded-host) is appended. | | autoOpen | boolean | false | Open the panel as soon as the widget mounts. | | baseUrl | string | — | Agent API origin when not fully encoded in the DSN. | | accountToken | string | — | Optional host auth token forwarded on API calls. | | project | { name, logo?, … } | — | Branding shown in the panel header. | | panelMode | 'sidebar' \| 'floating' \| 'fullscreen' | 'sidebar' | Initial panel layout. | | panelModeSwitchable | boolean | true | Let users switch layout from the header menu. | | pageContext | AgentChatPageContext | — | Extended business context (entity, metadata, …). | | attachPageContext | boolean | false | Default for the page-context visibility checkbox. Lightweight page anchors are sent for Auto-Agent and page tools. | | debug | boolean | false | Verbose console logs and unhandled-error warnings. | | headers | Record<string, string> | — | Extra HTTP headers added to every backend call. | | onReady | () => void | — | Convenience alias for instance.on('ready', ...). |

Events

const off = AgentChat.on('message', ({ message, source }) => {
  console.log(source, message.content.text);
});
off(); // unsubscribe

| Event | Payload | Notes | | --- | --- | --- | | ready | void | Fired after init() settles. | | error | { code, message, cause? } | Any SDK-level error. Subscribe in production to forward into Sentry/Datadog. | | open / close | void | The chat panel opened/closed. | | session | { sessionId, conversationId } | Active chat session changed. | | sessions | { sessions: ChatSessionSummary[] } | Sidebar session list refreshed (GET /chat). | | message | { message, source } | A user, agent, or system message hit the timeline (agent may stream incrementally). | | hostAction | { action } | Agent requests a host-page side effect (refresh data, navigate, …). | | pageAgentLifecycle | PageAgentLifecyclePayload | DSL tool lifecycle (start / stream / complete / error). Used by useAppAgent / usePageAgent. | | pageAgentAwaitingApproval | PageAgentAwaitingApprovalPayload | PageAction paused at awaiting_approval. | | pageActionAwaitingApproval | PageActionAwaitingApprovalNoticePayload | Built-in panel notification (open inbox CTA). | | approvalInboxChanged | void | Inbox list should refresh. | | openAutomationTaskCenter | void | Open the automation task center drawer. | | automationTaskCenterChanged | void | Task center list/badge should refresh. | | pageActionSubmitted | PageActionSubmittedPayload | PageAction invoke accepted (JSON); includes optional sourceRect for launch animation. | | pageActionRowChanged | PageActionRowChangedPayload | Per-row list PageAction slot updated (rowKey, state). | | destroyed | void | The instance was torn down. |

Host page actions

See docs/host-page-context-and-actions.md for the full spec: pageContext request format, SSE host_action envelope, field alignment, and integration examples.

When the agent needs the host page to run browser-side tools, the backend emits SSE host_action with a hostTools list (instead of legacy type: refresh).

Backend envelope (SSE event host_action, or message with action: host_action):

{
  "action": "host_action",
  "scope": "campaign-detail",
  "entity": { "type": "campaign", "id": "123" },
  "hostTools": [
    { "name": "refreshEntity", "args": { "entityType": "campaign", "entityId": "123" } }
  ],
  "reason": "agent_mutation_success",
  "runId": 42,
  "turnId": 7
}
  • scope must match pageContext.page on the user message.
  • hostTools is required (server only pushes when non-empty).
  • reason: plan_host_tool (mid-run Plan step) or agent_mutation_success (after HTTP write).

React (recommended): useAppAgent + usePageAgent

Split declaration (app root) from execution (business page):

import { useMemo } from 'react';
import { useAppAgent, usePageAgent } from 'omnix-chat/react';

// App.tsx — declare global tools (metadata sync; handler optional)
const appTools = useMemo(
  () => [{
    name: 'refreshEntity',
    description: 'Refetch entity after agent writes',
    argsSchema: {
      type: 'object',
      properties: { entityId: { type: 'string' } },
      required: ['entityId'],
    },
    argsTemplate: { entityId: '$entity.id' },
  }],
  [],
);

function AppShell() {
  useAppAgent({
    tools: appTools,
    onComplete: () => analytics.track('entity_refreshed'),
  });
  return <Outlet />;
}

// CampaignDetail.tsx — execute + page-only tools + lifecycle
function CampaignDetailPage({ campaignId }: { campaignId: string }) {
  const { refetch } = useCampaignDetail(campaignId);

  const handlers = useMemo(
    () => ({
      refreshEntity: async ({ entityId }: Record<string, unknown>) => {
        await refetch(String(entityId || campaignId));
      },
    }),
    [refetch, campaignId],
  );

  usePageAgent({
    scope: 'campaign-detail',
    pageContext: { page: 'campaign-detail', entity: { type: 'campaign', id: campaignId } },
    handlers,
    tools: [{
      name: 'fillNoteDraft',
      description: 'Stream note draft into the form',
      argsSchema: { type: 'object', properties: { text: { type: 'string' } } },
      handler: ({ text }) => setDraft(String(text ?? '')),
    }],
    onStart: () => setFilling(true),
    onComplete: ({ hostTools }) => {
      setFilling(false);
      if (hostTools.some((t) => t.name === 'refreshEntity')) setBanner('Refreshed');
    },
    onAwaitingApproval: () => openInbox(),
  });

  return <CampaignView />;
}

| Hook | Where | Role | | --- | --- | --- | | useAppAgent | App root | Global tools metadata (appLevel → scope *; no generic: true needed). Optional onComplete for analytics. | | usePageAgent | Business page | handlers run app-declared tools locally; tools for page-only tools; lifecycle + onAwaitingApproval. |

Memoize tools and handlers with useMemo so handler identity changes do not re-sync metadata or re-run prepareSession. Handlers unregister on page unmount.

Auto-Agent selection

By default, the SDK does not send agentId when creating a new chat session. The backend can select the best available agent from the user message, permissions, and pageContext, then bind it to the session.

<AgentChat
  dsn="your-dsn"
  baseUrl="/agent-api"
  pageContext={{ page: 'order-detail', entity: { type: 'order', id: orderId } }}
>
  <App />
</AgentChat>

Behavior:

  • New sessions omit agentId unless defaultAgentId is configured or the user manually chooses an agent in the toolbar.
  • The agent selector shows an Auto mode option by default, similar to the skill selector's no-skill option. Switching back to Auto clears the explicit agentId.
  • pageContext is included as a lightweight page anchor, helping the backend match page-specific agents and host tools.
  • When POST /chat returns { agent: { id, source, reason } }, the SDK stores it as state.autoAgent and updates state.selectedAgentId.
  • Existing sessions restore selectedAgentId from GET /chat/:sessionId agentId, but the SDK does not resend that id as an explicit override.

Set defaultAgentId only when you want to force a specific initial agent and skip backend auto-selection.

@ context mentions (per message)

Users can attach selected page-context snippets to a single chat message (without changing global pageContext). The SDK merges chosen items into pageContext.metadata for that send only — the server reads metadata.{kind}.content as inline records.

Catalog sources

| Source | How it appears in the @ picker | | --- | --- | | pageContext | Current page anchor (page:anchor) | | routeContext | Current route (route:current) | | pageContext.metadata | Each key with readable body (string, { content }, or JSON) | | registerContextMentions() / useContextMentions | Host items (group: 'host'); Live badge when resolve() |

Only put in metadata what you want users to @. Plain strings (e.g. tab: 'budget') also appear in the picker.

Recommended — static snippets (definePageContextMentions, no manual keys):

import { definePageContextMentions } from 'omnix-chat';

instance.setPageContext({
  page: 'campaign-detail',
  entity: { type: 'campaign', id },
  metadata: {
    ...definePageContextMentions([
      { label: 'Campaign summary', body: 'ROI 2.4x, spend $12k…' },
      { label: 'Audience snapshot', body: 'US mobile, age 25–34…' },
    ]),
  },
});

Recommended — dynamic snippets (useContextMentions; resolve may return a string):

import { useContextMentions } from 'omnix-chat/react';

const mentionDefs = useMemo(
  () => [
    {
      label: 'Live campaign data',
      resolve: async () => `ROI ${row.roi}, spend $${row.spend}`,
    },
  ],
  [row],
);
useContextMentions(instance, mentionDefs);

Low-level registration (callable before init() — queued like setPageContext):

import { defineContextMention } from 'omnix-chat';

instance.registerContextMentions([
  defineContextMention({
    label: 'Selected campaign row',
    resolve: async () => `ROI ${row.roi}, spend $${row.spend}`,
  }),
]);

instance.unregisterContextMentions(['host:selected-campaign-row']);

Imperative send (panel UI does this automatically when the user picks @ items):

await instance.sendMessage('Summarize this', {
  contextMentions: [
    { id: 'meta:campaign-summary', label: 'Campaign summary', kind: 'campaign-summary', payload: { content: '…' } },
  ],
});

Composer UX: toolbar Attach page context + chips (horizontal scroll); type @ in the textarea for autocomplete. Exported: defineContextMention, definePageContextMentions, ContextMentionAttachment, ContextMentionDefinition, ContextMentionRegistration, RegisteredContextMention; React: useContextMentions.

useHostAction (optional guards)

hostTools are executed by the SDK via registered tool handlers. Use useHostAction only for extra logic keyed on reason or entity (toasts, guards) — not as a substitute for handlers:

import { useHostAction, HOST_ACTION_REASON } from 'omnix-chat/react';

useHostAction('campaign-detail', async (action) => {
  if (action.entity?.id && action.entity.id !== campaignId) return;
  if (action.reason === HOST_ACTION_REASON.AGENT_MUTATION_SUCCESS) {
    toast.success('Agent updated this page');
  }
});

dispatchHostAction ignores the streaming phase; use usePageAgent onStream for DSL progress.

Imperative / non-React pages:

import { registerHostAction, registerHostToolsWithSync, HOST_ACTION_REASON } from 'omnix-chat';

await registerHostToolsWithSync(chat, {
  scope: 'campaign-detail',
  tools: [{
    name: 'refreshEntity',
    description: 'Refetch campaign after agent writes',
    argsSchema: { type: 'object', properties: { entityId: { type: 'string' } } },
    handler: async ({ entityId }) => { await refetch(String(entityId)); },
  }],
});

const off = registerHostAction('campaign-detail', (action) => {
  if (action.reason === HOST_ACTION_REASON.AGENT_MUTATION_SUCCESS) loadCampaignData();
});
// later: off();

The SDK also emits instance.on('hostAction', ({ action }) => …) and instance.on('pageAgentLifecycle', …) if you prefer the event bus.

Low-level host tool API

import {
  registerHostTool,
  unregisterHostTool,
  registerHostToolsWithSync,
  type HostToolDefinition,
  type AppHostToolDefinition,
} from 'omnix-chat/react';

registerHostTool('campaign-detail', 'refreshEntity', async (args) => {
  await refetchCampaign(args.entityId as string);
});

await chat.registerHostToolsWithSync({
  appLevel: true,
  tools: [{
    name: 'refreshEntity',
    description: 'App-wide refresh tool metadata',
    argsSchema: { type: 'object', properties: { entityId: { type: 'string' } } },
  } satisfies AppHostToolDefinition],
});

registerHostToolsWithSync calls POST /host-tool/client/register so the agent knows which tools exist. prepareSession runs only when new tools are registered or new metadata is synced.

Migration (v1.7.0): Prefer useAppAgent + usePageAgent over registering generic: true tools manually. App tools do not need generic — the hook implies app-wide scope.

Migration (v1.5.0): host_action no longer requires status: 'completed'. Payloads must include non-empty hostTools and a scope matching pageContext.page. See CHANGELOG.

Page context

Route information (routePath, routeParams) and lightweight page anchors (page, entity, metadata, …) are attached to user messages so Auto-Agent and page tools can match the current surface.

<AgentChat
  pageContext={{ page: 'campaign-detail', entity: { type: 'campaign', id } }}
  attachPageContext={false}
/>
instance.setPageContext({ page: 'campaign-detail', entity: { type: 'campaign', id } });
instance.setAttachPageContext(true); // optional UI display preference

The input footer shows a preview icon: inspect route + extended fields and the JSON that will be sent on the next message. Full spec: docs/host-page-context-and-actions.md.

Write confirmation

When the agent proposes a destructive write, the backend emits confirmation_required on the SSE stream. The SDK shows an inline card on the assistant message (not a modal) with confirm / cancel actions.

// Programmatic (same as clicking the inline buttons)
await instance.confirmWrite();
await instance.cancelWrite();

Listen for UI state via getState().writeConfirmation or subscribe to state changes after confirmation_required / complete events.

Chat write gates do not appear in the approval inbox — confirm or cancel only via the inline bubble or confirmWrite() / cancelWrite() (Redis gate + SSE).

Automation approval inbox

PageAction (and webhook) workflow gates surface as pending approvals in the inbox. The panel shows a bell icon (badge = pending count) that opens the drawer.

// Headless / custom UI
await instance.refreshApprovalInbox();
const detail = await instance.getApproval(12);
const { resumed } = await instance.confirmApproval(12); // page_action: sync resume in API
await instance.rejectApproval(12, 'optional reason');

const { approvalInbox } = instance.getState(); // { items, loading }

instance.on('approvalInboxChanged', () => {
  void instance.refreshApprovalInbox();
});

| Source | Hang-up signal | Confirm in SDK | | --- | --- | --- | | chat | SSE confirmation_required + inline bubble | confirmWrite() / cancelWrite() only (not inbox) | | page_action | invokePageAction → { awaitingApproval: true } | Inbox: decideApproval(id, …) / confirmApproval / rejectApproval |

prefetch() refreshes the inbox once per login. Draft review (edit / retry / cancel) uses POST /approval/:id/decide. Detail view shows reference (entityReference) beside proposed write (writeDraft). While a decide request is in flight, the detail panel keeps the current snapshot until the API returns. See agent-server c-end-page-action-approval-interaction.md and docs/approval-inbox-client-integration.md.

Automation task center

PageAction runs appear in the launcher task center (history icon + badge). Updates are event-driven (invoke + run SSE lifecycle) — no background polling. Login / init still best-effort fetches GET /automation/tasks?status=active to restore badge and pageActionKey.

await instance.refreshAutomationTasks({ status: 'active' });
const detail = await instance.getAutomationTaskDetail(runId);
await instance.hydrateAutomationTaskDetail(runId);

instance.openAutomationTaskCenter();
instance.on('openAutomationTaskCenter', () => { /* drawer opened */ });
instance.on('pageActionSubmitted', (payload) => {
  console.log(payload.runId, payload.status);
});

const { automationTaskCenter } = instance.getState();
// { items, activeCount, statusFilter, selectedDetail, … }

| Trigger | Behaviour | | --- | --- | | Login / init | GET /automation/tasks?status=active (restore badge + pageActionKey) | | User opens task center / manual refresh | GET /automation/tasks | | invokePageAction accepted | Optimistic list row + badge (in-memory registry) | | Run SSE page_action lifecycle | Patch status in memory | | Tab visible + stale registry (> 60s) | reconcileAutomationTasksIfNeeded() once |

Server spec: agent-server docs/automation-c-end-task-center-integration.md.

PageAction invoke (JSON + run SSE)

v2.1.0 breaking: POST /page-action/brain-invoke (formerly /page-action/invoke) returns JSON (runId, status, streamUrl, …); the run stream is GET /page-action/runs/:runId/stream.

React — non-blocking trigger with phases + optional launch animation:

import { useRef } from 'react';
import { usePageAction } from 'omnix-chat/react';

const btnRef = useRef<HTMLButtonElement>(null);

const pageAction = usePageAction({
  actionKey: 'demo-playground.fillnotedraft',
  pageContext: { page: 'demo-playground', entity: { type: 'campaign', id: '42' } },
  defaultInstruction: 'Generate a friendly note draft',
});

<button
  ref={btnRef}
  className="ac-page-action-trigger"
  disabled={pageAction.disabled}
  onClick={() => void pageAction.invoke({ launchAnchor: btnRef.current })}
>
  Run Page Bot
</button>
<button type="button" onClick={pageAction.openTaskCenter}>Task center</button>

| phase | Meaning | | --- | --- | | idle | Ready | | submitting | POST /page-action/brain-invoke in flight | | launched | Accepted; optional fly-to-task-center animation | | streaming | Background run SSE active | | awaiting_approval | Paused for inbox approval (not counted as loading) | | completed / failed | Terminal (auto-reset to idle) |

loading = busy execution only. disabled = loading || awaitingApproval. When approval is required, the built-in notification offers Open approvals (inbox drawer only — does not open the chat panel). Server dedup uses pageActionKey; duplicate invoke while awaiting returns { awaitingApproval: true, approvalRequestId, … } instead of throwing.

List pages (usePageActions, v2.2.2+)

When multiple table rows share one actionKey, use usePageActions — one UI slot per rowKey (typically String(entity.id)). Do not share a single usePageAction disabled across rows.

import { usePageActions } from 'omnix-chat/react';

const pageActions = usePageActions({
  actionKey: 'campaign-list.fillDraft',
  page: 'campaign-list',
  requireInstruction: false,
});

{rows.map((row) => {
  const s = pageActions.getRow(String(row.id));
  return (
    <button
      key={row.id}
      disabled={s.disabled}
      onClick={() =>
        void pageActions.invoke({
          rowKey: String(row.id),
          pageContext: {
            page: 'campaign-list',
            entity: { type: 'campaign', id: String(row.id) },
          },
          instruction: `Summarize ${row.name}`,
        })
      }
    >
      {s.awaitingApproval ? 'Awaiting approval…' : s.loading ? 'Running…' : 'Generate'}
    </button>
  );
})}
  • Mount reconcile: GET /automation/tasks?status=active&actionKey=
  • Imperative: getPageActionRowState, reconcileActivePageActions, hydratePageActionByRunId, clearPageActionRowError; event pageActionRowChanged
  • List usePageAgent: prefer pageContext: { page } without a fixed entity, or other rows' host_action frames may be gated out. See demo/DemoHostBridge.tsx.

Detail / single-entity pages keep using usePageAction (thin single-slot wrapper).

Host integration tips (list PageAction)

| Topic | Recommendation | | --- | --- | | Import | Umi 3: omnix-chat/react/legacy; Vite / Umi 4: omnix-chat/react. Never omnix-chat/dist/*.js. | | Peers | Only react + react-dom; do not install antd / @ant-design/x in the host app. | | rowKey | Align with entity.id, e.g. String(row.id). | | usePageAgent | On list pages use pageContext: { page } without a fixed entity, or host_action for other rows is gated out. | | Row buttons | Use usePageActions + getRow(id); do not share one usePageAction disabled across rows. | | After approval | Inbox confirm / cancel updates the matching row slot automatically (streaming / idle). | | Notifications | Built-in toasts appear top-right (awaiting approval, execution result, background reply). | | Demo | demo/DemoHostBridge.tsx list section; Umi details in docs/umi-integration.zh-CN.md. |

Imperative (detail page — body field instruction):

const result = await instance.invokePageAction({
  actionKey: 'demo-playground.fillnotedraft',
  pageContext: { page: 'demo-playground', entity: { type: 'campaign', id: '42' } },
  instruction: userInstruction,
  clientActionId: 'unique-client-id',
  awaitStream: false, // default in usePageAction — SSE consumed in background
  launchAnchor: buttonEl,
  onChunk: (text) => { /* optional preview from arg.append */ },
});

if (result.awaitingApproval) {
  instance.openApprovalInbox(
    result.approvalRequestId != null ? { approvalId: result.approvalRequestId } : undefined,
  );
}

Streams host_action (DSL fill) and page_action lifecycle on the run SSE connection.

Host Markdown (<Markdown />, v2.2.5+)

Render PageAction analysis output or any GFM text without installing react-markdown:

import { Markdown } from 'omnix-chat/react';

const [report, setReport] = useState('');

<Markdown content={report} className="my-report" />

Headless: import { markdownToSafeHtml } from 'omnix-chat'. Approval inbox detail, draft review, and automation task previews use the same renderer internally.

Business-page state subscription (v2.2.5+)

usePageActions / usePageAgent / useAppAgent no longer call useAgentChat() internally — they do not re-render on every chat SSE tick. For custom hooks:

import { useAgentChatClient, useAgentChatStore } from 'omnix-chat/react';

const instance = useAgentChatClient();
const isLoggedIn = useAgentChatStore((s) => s.isLoggedIn);

Use useAgentChat() when you need the full panel state (messages, sessions, etc.).

Panel layout

| Mode | Behaviour | | --- | --- | | sidebar | Docked panel with session rail on the left (default). | | floating | Draggable window with macOS-style close / minimize / fullscreen controls. | | fullscreen | Occupies the host viewport; session rail collapses to a slim icon strip. |

In sidebar and floating, the session rail is 116px when expanded (titles visible) and 40px when collapsed. Use panelModeSwitchable: false to lock the layout, or instance.setPanelMode('floating') imperatively.

Lifecycle

AgentChat.init({ dsn });
AgentChat.open();
AgentChat.identify({ id: 'u-99' });
// Resolves with the **user** message once POST succeeds.
// Assistant replies arrive via `message` events (SSE stream per sessionId).
const userMsg = await AgentChat.sendMessage('Hello!');

AgentChat.selectSession('existing-session-id'); // switch sidebar session

AgentChat.destroy();    // clean up DOM, abort in-flight requests
AgentChat.init({ dsn }); // safe to re-init after destroy

For multi-tenant pages use createAgentChat() to obtain independent instances.

React Strict Mode

If you call the imperative AgentChat.init() from inside a React useEffect under <StrictMode>, dev-mode will run setup → cleanup → setup, which otherwise causes the launcher to flash on screen. Prefer <AgentChat dsn="..." /> from omnix-chat/react, which handles init/teardown for you. If you still call the imperative API from an effect, guard with a deferred destroy:

const ref = useRef<{ key: string | null; timer: number | null }>({ key: null, timer: null });

useEffect(() => {
  if (ref.current.timer != null) {
    clearTimeout(ref.current.timer);
    ref.current.timer = null;
  }
  if (ref.current.key !== dsn) {
    if (ref.current.key) AgentChat.destroy();
    AgentChat.init({ dsn });
    ref.current.key = dsn;
  }
  return () => {
    ref.current.timer = window.setTimeout(() => {
      AgentChat.destroy();
      ref.current.key = null;
      ref.current.timer = null;
    }, 0);
  };
}, [dsn]);

Multi-session chat & streaming

The widget shows a left sidebar of conversations (GET /chat). Selecting one loads GET /chat/{sessionId} into the message pane.

sendMessage() pipeline (SDK)

User hits Send
  → optimistic user bubble (pending) + notifyStateChange     ← UI shows immediately
  → ensure sessionId
  → GET /chat/{sessionId}/stream (SSE connected)
  → POST user message (existing session only)
  → SSE events → agent bubble updates + notifyStateChange    ← UI streams assistant

Existing session (has activeSessionId):

| Order | HTTP | Purpose | | --- | --- | --- | | 1 | GET /chat/{sessionId}/stream | Listen before agent runs | | 2 | POST /chat/{sessionId}/messages | { role: "user", content } triggers agent | | 3 | same SSE | think / result / complete / error |

First message (no session yet):

| Order | HTTP | Purpose | | --- | --- | --- | | 1 | POST /chat | { role, content } → { sessionId } + starts agent (backend contract) | | 2 | GET /chat/{sessionId}/stream | Subscribe (ReplaySubject replays recent events) | | — | skip POST .../messages | User text already persisted in step 1 |

Other APIs

| Step | HTTP | Notes | | --- | --- | --- | | List sessions | GET /chat | Sidebar | | Load session | GET /chat/{sessionId} | selectSession() / prefetch() |

SSE → UI mapping lives in src/core/chat-sse.ts. React reads getState().messages via useClientState in ChatBody (useSyncExternalStore + notifyStateChange).

sendMessage() resolves when the user bubble is sent or failed. Assistant content streams through repeated message events and state updates.

Backend HTTP contract (legacy v1 sketch)

The SDK historically documented two endpoints:

POST {apiBase}/sessions

Headers: X-Agent-Public-Key, X-Agent-Project-Id, X-Agent-Sdk-Version, Content-Type: application/json.

Body:

{
  "locale": "zh-CN",
  "sdkVersion": "2.2.9",
  "origin": "https://app.example.com",
  "user": { "id": "u-123", "email": "[email protected]" },
  "existingConversationId": "conv_abc"
}

Response (200):

{
  "sessionId": "sess_...",
  "sessionToken": "tok_...",
  "expiresAt": 1731567890000,
  "conversation": { "id": "conv_...", "messages": [] }
}

POST {apiBase}/messages

Headers: Authorization: Bearer <sessionToken>, plus the same X-Agent-* headers.

Body:

{
  "conversationId": "conv_abc",
  "content": { "type": "text", "text": "Hello!" }
}

Response (200):

{
  "message": {
    "id": "msg_...",
    "role": "agent",
    "content": { "type": "text", "text": "Hi there!" },
    "createdAt": 1731567890000,
    "status": "sent"
  }
}

Errors should follow the shape { "error": { "code": "STRING_CODE", "message": "Human readable" } } so the SDK can surface them as typed AgentChatError instances.

Security

  • The DSN's publicKey is public. Any rate-limiting or origin allow-list must be enforced server-side (the SDK forwards Origin in the handshake body so you can check it).
  • For trusted user identification pass an HMAC of the user id as user.signature (validate it on the backend with a shared secret).
  • The SDK never executes eval or new Function. Static widget CSS is injected once under #agent-chat-embedded-styles with every selector prefixed by .ac-embedded-host; antd css-in-js stays inside .ac-embedded-mount. CSP typically needs style-src 'unsafe-inline' for css-in-js.

Local development

pnpm install
pnpm dev          # runs the demo playground at http://localhost:5173
pnpm build        # produces dist/omnix-chat.es.js + .umd.js + react.es.js + .umd.js + types
pnpm build:watch  # rebuild dist/ on src/ changes (use while link-debugging in another app)
pnpm pack:local   # build + pack/omnix-chat-x.y.z.tgz for offline install
pnpm typecheck
pnpm size         # asserts gzipped budgets

Link into your local app (pnpm)

# SDK repo — build once (or run build:watch in another terminal)
cd /path/to/agent-chat
pnpm install && pnpm build

# Your app — symlink the package
cd /path/to/your-app
pnpm add "link:/path/to/agent-chat"

In your app:

import { AgentChat } from 'omnix-chat/react';

While developing the SDK, keep pnpm build:watch running in the agent-chat repo; your app picks up changes after each rebuild (Vite may need a refresh).

Tarball instead of symlink: pnpm pack:local then pnpm add /path/to/agent-chat/pack/omnix-chat-*.tgz.

See examples/react/README.md for a minimal consumer app.

The playground intercepts /api/edge/*/v1/sessions and /api/edge/*/v1/messages via a built-in Vite middleware, so you can develop without a real backend.

License

MIT