Skip to main content
Headless React hooks that read from the same wire the built-in chrome uses. Pair them with your own UI, or drop them beside VendoOverlay to power an inline widget.
Every hook here is also re-exported from @vendoai/vendo/react, which is the import that needs no direct @vendoai/vendo/ui dependency — with one exception. useApprovalModal ships only on @vendoai/vendo/ui/chrome:

The shared read shape

Collection hooks all return the same read fields, so one pattern renders loading, error, and empty states across the whole surface.
The collection is seeded, not undefined, so threads.length === 0 means empty or not loaded yet. isLoading is what tells those apart, and it only ever flips for the first fetch — a first-mount spinner never flashes again on a poll. A failed read keeps the last good collection, so you can render a retry affordance without blanking the surface. useApp is the exception. It returns a single document, so its app is AppDocument | undefined.

Every hook

There is no generic execute callback. Hooks that write name their verbs: decide, revoke, disconnect, enable, disable, create, remove, fork, importApp, edit, dryRun, stopRun. Each returns a promise, and the ones that change the collection refresh it when they resolve. exportApp is the exception, being a pure read that refreshes nothing. A hook’s isLoading still tracks only the first read, so drive mutation-pending UI from your own await.

Context accessors

There is no useVendo. useVendoProvider is the React-context accessor, because useVendoContext(data) owns the agent-context name.

Polling

Pass pollMs to keep a value fresh without a manual refresh. Polls are self-scheduling rather than interval-driven, so the next tick arms only after the current refresh settles and a slow server never stacks requests.
Omit pollMs for a one-shot fetch on mount. Polling does not pause when the tab is hidden, so pick a cadence you are willing to pay for in the background. useApp and useVendoStatus never poll. useApp still returns refresh(); useVendoStatus reads once per mount, and remounting is the only way to re-read it. useSlotApp is the opposite: it polls every 5 seconds by default, because a placement made in the conversation surface has to appear in the slot on its own. Pass { pollMs } to change the cadence, or { enabled: false } to stand it down. useApprovals and useSlotApp each share one poller per client across every mounted instance, so a page with ten slots still makes one request.

Threads

useThreads reads the same summaries VendoOverlay uses, so a custom conversation list keeps parity with the shipped chrome.
ThreadSummary.title is always a string, so no ?? "Untitled" fallback is needed. Pair it with useVendoThread(threadId) to drive the streaming turn. It wraps the AI SDK’s useChat, so its vocabulary is the AI SDK’s.
The argument is named selectedThreadId on purpose. The hook also returns threadId, so destructuring into that name while passing it in is a use-before-declaration error. setMessages is what an edit-last affordance is built on: drop the last user turn and anything after it, then refill your input from that message. This is the flow the shipped chrome’s Edit affordance uses.
The hook does not queue sends. Calling sendMessage mid-stream hands the message straight to the AI SDK. The “type while it is answering, and it sends when the reply lands” behavior belongs to the shipped chrome’s composer, which holds the draft and re-sends it on the busy edge. Reproduce it by watching status.

Standalone agent chat

useVendoChat is useVendoThread’s thinner sibling, for a page talking to an @vendoai/vendo mount. No provider, no client, no embed chrome, no [Context] block — just the transport, the thread-id round trip, and the two things you have to render for an agent that asks permission.
options is { api, threadId?, onThreadId? }, where api is where you mounted handler(). It keeps nothing in the browser. Reopening a thread reads the transcript back through the mount’s own route, so an approval parked before a reload comes back in interruptions with no client state to have lost. resume(decisions) posts to the mount’s approvals wire rather than the AI SDK’s local approval channel: the guard’s decision is what unblocks the parked call, and flipping the part in the browser would change what the page draws and nothing about what the agent is doing. A decision that does not land — a 409 for an ask already answered or expired — throws instead of being swallowed.

Apps export and import

useApps exposes exportApp(appId) and importApp(bytes) beside the read fields, so a custom drawer can round-trip an AppDocument without hand-rolling calls to /apps/:id/export and /apps/import.
Import mints a fresh app_ id and carries over no data, grants, or authority.

Overlay control

useVendoOverlay gives your own chrome programmatic control over VendoOverlay.
useVendoOverlay(options?) accepts one option, defaultOpen?: boolean. Spread overlayProps onto the component and call open, close, or toggle from your own shortcut.
The panel portals to document.body, locks page scroll, marks the page behind the scrim inert, and restores focus to the invoking element on close. Closing hides the panel without discarding the conversation, so reopening within the same page session restores the prior messages. Call overlay.newConversation() to start fresh.

Approval modal

useApprovalModal is the mount seam for the screen-initiated approval modal, the centered ask a person sees when a button inside a generated view parks on the guard. The shipped chrome already mounts it on VendoSlot, on in-thread app cards and the workspace stage, on the chat embeds, and on mounted remix forks. Reach for the hook when you render a TreeView, AppFrame, or a bespoke slot yourself.
Wire it in two lines. Pass approval.onParked down to whichever component fires it, and render approval.modal alongside.
Presses queue by design. Pressing several guarded buttons raises several approvals, and exactly one modal is on screen at a time. Approve or Deny spends the decision. Esc and the scrim close the modal without deciding, so a dismissed ask stays pending.

refusalCopy

refusalCopy(reason) maps an error from approvals.decide to the same user-voice sentence the built-in approval card renders when a decision fails to land.

Hooks or chrome

Reach for VendoOverlay and the other chrome components when you want the shipped surface with brand tokens applied. Reach for hooks when you need counts, badges, or lists inside your own layout, or when your chrome has to react to Vendo state without rendering the overlay at all. Both paths speak the same wire, so mixing them in one app is safe. If you run your own agent loop and spread in the guarded tool pack, a separate set of components renders Vendo inside that chat instead: VendoToolResult, VendoAppEmbed, and VendoApprovalEmbed, all on Embeds and envelopes.