@agility/vibe-embed
v0.1.0
Published
Embed the Agility vibe-coding chat in a React app — widget lifecycle, a versioned host↔widget protocol, and a provider/hook pair.
Downloads
84
Readme
@agility/vibe-embed
Embeds the Agility vibe-coding chat into a React app as an Intercom-style widget: an iframe the host controls, a versioned message protocol between them, and a provider/hook pair so any component can open the chat or read what it's doing.
The first host is Agility's own Content Manager app (React 18, Vite), behind a feature flag.
Install
npm install @agility/vibe-embed # yarn add @agility/vibe-embedRequires React >= 18 as a peer. Public npm, same scope and registry as @agility/plenum-ui
and the rest — no .npmrc entry and no auth token, in the app or in CI.
Why not GitHub Packages: it requires the package be scoped to the repo owner (
@agility), and npm maps a registry per scope, not per package. Pointing@agilityat GitHub Packages would sendplenum-ui,content-fetch,nextjs,management-sdkand the rest there too, and they'd all 404. Using GitHub Packages for this SDK would mean moving the entire scope.
Use
Mount the provider once at the app shell — behind the flag, so flag-off never renders it:
import { AgilityVibeProvider, useAgilityVibe } from "@agility/vibe-embed"
<AgilityVibeProvider
widgetUrl="https://vibe-coding.agilitycms.com/widget"
instanceGuid={instance.guid}
auth={() => getManagementToken()} // called lazily, and again on iframe reload
context={{ url: location.pathname, pageID }}
onNavigate={(e) => router.push(e.url)}
>
{children}
</AgilityVibeProvider>Then anywhere below it:
const vibe = useAgilityVibe()
if (!vibe) return null // null until the provider has mounted — see below
vibe.toggle()
vibe.state.busy // true while a turn is runningThe things worth knowing before you wire it up
useAgilityVibe()returnsnullon the first render. The widget instance is created in an effect, so the context has no handle until after mount. Guard it. It is typed| nullrather than throwing so a launcher can render its own disabled state instead of exploding in a host app that merely rendered early.authreturns the logged-in CMS user's management token, and is called lazily and again whenever the iframe reloads — so a short-lived token is fine, and expected. Identity is re-derived server-side from the token presented; nothing trusts a client-claimed user id.- Options are captured at mount, deliberately.
widgetUrlorinstanceGuidchanging means a different widget, not a reconfigured one — remount with akeywhen the user switches instance. renderLauncher={false}if the host has its own launcher. Readstate.busy/state.openand drive it yourself; the plan calls for the launcher to show when a chat is processing.contextis pushed on every change — a SPA route change is asetContext, which is how the agent knows what the user is looking at. It's context, not a request: the agent is told never to act on it by itself.
Protocol
Host↔widget messages are versioned and namespaced (PROTOCOL_SOURCE, PROTOCOL_V) and every
inbound message is validated by parseWidgetMessage before it reaches state — an iframe on
another origin is untrusted input. reduceState is pure and exported, so the host can model
widget state without a live iframe (that's what the tests do).
Publishing
pnpm --filter @agility/vibe-embed publish --access publicprepack builds first, files ships dist alone, and publishConfig repoints
main/types/exports at dist — on publish only.
In this monorepo the package stays source-resolved (main → src/index.ts), so the
dashboard keeps importing TypeScript directly with no build step and no dist in the dev path
to go stale. That split is the whole reason publishConfig exists here.
Nothing secret ships: this is browser code that a host serves to its own users. Being on public npm doesn't open the widget to the world either — origin and auth are enforced server-side, and public embedding in customers' apps remains a separate decision (see docs/plans/chat-widget.md).
