VendoOverlay to power an inline widget.
@vendoai/vendo/react, which is the import that needs no direct @vendoai/ui dependency — with one exception. useApprovalModal ships only on @vendoai/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.undefined. So threads.length === 0 means empty or not loaded yet, and isLoading is what tells those apart.
error is undefined, not null, when the read succeeded. A failed read keeps the last good collection, so you can render a retry affordance without blanking the surface.
isLoading is true only for the first fetch. Later refresh() calls and background polls do not flip it back, so a first-mount spinner does not flash on every update.
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 context accessor, because useVendoContext(data) owns the situation-channel name.
Polling
PasspollMs 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.
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.
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.
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.
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.
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.
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.
approval.onParked down to whichever component fires it, and render approval.modal alongside.
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 forVendoOverlay 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.