@antsomicorp/henos-embed-sdk
v0.1.5
Published
A framework-independent browser ESM package for mounting the real Henos chat in an iframe. No build step or runtime dependencies are needed. TypeScript declarations ship alongside the JavaScript.
Readme
Henos Embed SDK
A framework-independent browser ESM package for mounting the real Henos chat in an iframe. No build step or runtime dependencies are needed. TypeScript declarations ship alongside the JavaScript.
Install from npm:
npm install @antsomicorp/henos-embed-sdk@^0.1.5For local development, use a package-manager file dependency:
{
"dependencies": {
"@antsomicorp/henos-embed-sdk": "file:../deer-flow/packages/henos-embed-sdk"
}
}Mount plain chat in an element with a defined height:
import { mountHenosChat } from "@antsomicorp/henos-embed-sdk";
const chat = mountHenosChat({
container: document.querySelector("#chat"),
url: "https://henos.example/embed/chat",
});
await chat.sendPrompt("Hello");
// On component unmount:
chat.destroy();Henos must permit the host application's exact origin through its operator configuration. Users sign in through Henos; do not pass server credentials to the SDK. The SDK adds the host's parentOrigin URL parameter automatically.
Context and host actions
Both callbacks are optional. Each chat submission requests fresh context through getContext({prompt}). Without that callback, context defaults to null, or to the latest JSON snapshot passed to setContext. When supplied, the callback is authoritative, including an intentional null return.
const chat = mountHenosChat({
container: document.querySelector("#chat"),
url: "https://henos.example/embed/chat",
getContext: async ({ prompt }) => ({
selectedItem: getCurrentSelection(),
revision: getCurrentRevision(),
}),
onAction: async ({ actionId, threadId, runId, context, action }) => {
// Validate the application's action schema, permissions and context revision.
// Return saved only after persistence has succeeded.
return { status: "rejected", message: "This action is not supported." };
},
onEvent: (event) => console.log(event),
});
await chat.setContext({ selectedItem: "example" });onAction returns {status, message, revision?}. Status is saved, applied, rejected, or error. The SDK does not interpret application action schemas. Without a handler, actions receive rejected. Repeated action IDs share a single result for the lifetime of a mount, including iframe reconnects; reusing an ID with different content is rejected. The host must still enforce its own persistent idempotency and concurrency checks.
setContext and sendPrompt return promises that resolve when their messages are posted after readiness. They do not acknowledge a completed model run or host mutation. destroy() removes the iframe and listeners, closes the channel, rejects pending commands, and suppresses late replies. It is safe to call repeatedly.
timeoutMs defaults to 30,000 and bounds readiness and host callback waits. A timeout or teardown cannot undo an application callback that has already started. An action that times out stays deduplicated; check the host state before any deliberate retry using a new ID. JSON payloads are bounded to 1,000,000 serialized characters and depth 64; functions, non-finite numbers, circular references, and non-JSON objects are rejected.
Protocol v1
All messages use {protocol:'henos-embed', version:1, type, id?, replyTo?, payload?}. The parent transfers a fresh MessagePort to the iframe's exact HTTP(S) origin with henos:init and {sessionId}. The child must validate the parent origin and source before accepting the port. On client initialization the child may send henos:hello; the SDK checks its exact origin and iframe source and repeats initialization, covering delayed hydration. The child acknowledges with ready on the port.
| Direction | Type | Payload / correlation |
| -------------- | ------------------ | ------------------------------------------------------------- | ------- | ----------------------------------------------- |
| Child → parent | prepare | {prompt} with id |
| Parent → child | prepare | {context} or {error:{code,message}}, with replyTo |
| Child → parent | action.request | {actionId,threadId,runId,context,action} with id |
| Parent → child | action.result | {status,message,revision?} with replyTo |
| Parent → child | prompt.send | {prompt} |
| Parent → child | context.update | {context}; informational, preparation remains authoritative |
| Parent → child | placement.update | {placement:'left' | 'right' | 'in-content',canChange}; confirmed host layout |
| Parent → child | artifact.dismiss | {expansionId}; close only that artifact expansion |
| Child → parent | event | {name,data?}; named application events only |
onEvent receives {type:'ready'}, {type:'error',payload:{code,message}}, or {type:'event',payload:{name,data?}}. Internal chat stream messages are not forwarded. Unknown protocol versions, unrecognized envelope types, and malformed requests are ignored. After reconnect, messages from the old port and late results are ignored.
Run transport tests with node --test packages/henos-embed-sdk/test/sdk.test.js from the repository root. These use simulated iframe/channel primitives and exercise the actual SDK; they do not substitute for browser authentication or a live model integration test.
sendPrompt() resolves when the prompt is sent to the frame. If chat is busy or loading history, onEvent receives an event with payload.name = "prompt.rejected"; the host can retry after the active run finishes.
Optional close control
Set showClose: true to show a close button in the embedded header (default:
false). Handle {type: "event", payload: {name: "close.requested", data: {}}}
in onEvent to hide the host panel. This is a local message through the existing
channel, with no API round trip. The SDK does not hide or destroy the iframe.
To preserve the draft and connection when reopening, keep the mount alive and
hide its container; call destroy() only when disposing of the integration.
The host can omit its duplicate title/close header when using this control.
onCloseRequest() handles the close button without parsing generic events.
Supplying onDisplayModeRequest(mode) enables the inline/popup toggle. The host
applies its layout, then calls chat.setDisplayMode(mode) to confirm the actual
mode (inline or popup). displayMode sets the initial mode. Confirmed state is
resent on iframe reconnect; requests alone never change the icon. These callbacks
also retain the existing generic onEvent notifications.
The host owns left/right/in-content placement and any persisted layout preference.
Pass placement and onPlacementRequest(placement) to show the three placement
icons in the embedded header. A click emits placement.requested; the host
updates its layout, then calls chat.setPlacement(placement) to confirm the
active icon. These values are resent after iframe reconnect. The SDK does not
move its own container, and switching placement should not remount the iframe.
Hosts without onPlacementRequest keep their existing header without placement
icons. Inline/popup remains a separate display-mode control.
Completed embedded runs emit tool.result events with
{toolCallId, toolName, structuredContent, threadId, runId} from actual current-turn
MCP artifacts. Host apps validate their contract and scope before refreshing;
assistant text is not parsed. Reopening history does not replay these events.
This is a notification after the run, not a durable delivery queue or a second write.
Managed popup and Mini (SDK 0.1.5+)
Opt in with popup: true when mounting. With displayMode: "popup", the SDK
positions the supplied container within the host viewport. Drag its title to
move it; drag edges/corners to resize. Focus the title or a resize handle and
use arrow keys (Shift for larger steps) for keyboard operation. The header's
Minimize button collapses the same chat to a 48px-high prompt-only window.
Focusing the prompt reveals the title above it (88px total, or 128px with an
answer preview). Moving focus to title controls keeps it open; clicking outside
collapses it again. Growth is upward and viewport-clamped;
Restore returns to the previous full height. Mini keeps its width, supports
horizontal resize and continues the existing run. It does not open itself when
a response arrives. Errors and waiting-for-reply status remain visible.
Clicking the answer preview or model picker restores the full popup. The normal
embedded composer is also shorter, with 13px input/placeholder text.
const chat = mountHenosChat({
container: document.querySelector("#chat"),
url: "https://henos.example/embed/chat",
popup: true,
displayMode: "popup",
});
await chat.setMinimized(true);
await chat.setMinimized(false);The host must provide a dedicated container whose fixed positioning is relative
to the viewport, without transformed/contained ancestors, another popup backdrop,
or a competing outer resize handle. Put popup decoration on this container, not
on a larger wrapper that would remain visible behind Mini. Inline/in-content
layout and visibility still belong to the host. Hosts with an existing shell
(such as Data Intelligence) can pass popup: { container: shellElement } to manage
the outer shell while keeping container as the inner iframe mount. The shell
must contain the mount and belong to the same document. Keep the inner mount
full-height/flex-growing and disable the host's own resize handles in popup mode.
Merely updating the npm dependency does not enable Mini.
Without onDisplayModeRequest, the SDK applies popup/inline requests itself.
With that callback, the existing host-confirmation contract remains: apply the
host layout then call setDisplayMode(). Docking restores original container
styles. By default geometry and Mini preference live only for this mount. To
restore them across reloads, pass popup: { container: shellElement, storageKey:
"my-app-chat" }. The SDK stores only resting position, full size and Mini state
in the host origin's localStorage, under henos-popup:v1:<storageKey>. Use distinct
keys per app/user when isolation is needed; never put credentials in a key.
Focus/answer-preview expansion is not saved. Restored geometry is clamped to the
current viewport; unavailable or malformed storage falls back to host defaults.
Mini and full popup use the same 16px outer radius. No chat content is persisted.
The additive v1 messages are parent popup.update with {enabled,minimized};
child events popup.preview.changed with {visible: boolean} and
popup.chrome.changed with {expanded: boolean} (select only 48/88/128px,
never arbitrary geometry or answer content),
popup.minimize.requested with {minimized} and popup.gesture
with {id,phase,edge?,x,y}. Phases are start, move, end, cancel; edges
are move, n, s, e, w, ne, nw, se, sw. Coordinates are pointer
screen coordinates in CSS pixels. Gestures are bounded and consumed by the SDK,
not forwarded to onEvent. Old SDKs do not advertise these controls. Reconnect
republishes confirmed state, and teardown restores styles/listeners. Artifact
expansion retains the existing host overlay and blocks popup manipulation until
closed. Requires the matching Henos embed frontend; workspace chat is unchanged.
Artifact overlays (0.1.4)
Expanding any artifact inside embedded chat uses a centered 95vw by 92dvh overlay with rounded corners and a dimmed, blurred backdrop on desktop. Below 768px it fills the viewport. The SDK promotes the existing chat iframe into a manual popover in the host's top layer; its DOM parent, URL, MCP session and filter state remain unchanged. The artifact fills the frame using the shared Henos expansion controls. No host callback or change to the inline/popup/placement preferences is needed.
The child emits event with name artifact.expansion.changed and data
{expansionId, expanded} on the authenticated embed MessagePort. Repeated opens
are idempotent; an old close cannot dismiss a newer expansion. Collapse or Escape
inside the artifact restores the prior iframe styles, scrolling and background
interactivity. Escape in the parent also sends artifact.dismiss for the active
ID. Reconnect and destroy() always release the overlay.
The inset overlay requires SDK 0.1.4 and the browser Popover API. SDK 0.1.3
retains its full-page host overlay; older SDKs and unsupported browsers retain
expansion within the chat iframe. Unsupported
browsers report ARTIFACT_OVERLAY_UNAVAILABLE through onEvent. The overlay does
not use the browser Fullscreen API or replay tool calls.
