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

@uiresponse/slot-sdk

v0.2.0

Published

React SDK for UI Response delivery slots: a UIResponseSlot renders a validated generated view, plain text, or your existing fallback — decided per ask.

Downloads

525

Readme

@uiresponse/slot-sdk

Put UI Response (UIR) inside an app you already have. A slot is one spot in an existing page where an answer may arrive as a live, validated view — or as plain text, or as nothing at all, decided per ask. Your app stays static by default; UIR enters only where you place it.

import { UIResponseSlot } from "@uiresponse/slot-sdk";
import "@uiresponse/slot-sdk/styles.css";

<UIResponseSlot
  id="artist-empty-state"
  ask="What should this artist's manager do next?"
  context={{ artist_id: artist.id }}
  lifetime="ephemeral"
  ui="auto"
  fallback={<EmptyState />}
/>

fallback is mandatory and paints immediately. A valid envelope then does exactly one thing:

  • text keeps the host fallback (usually the response text the host already had).
  • ui replaces it with the existing @uiresponse/web embed renderer.
  • empty renders nothing.

The default sidecar deadline is 2.5 seconds. Three consecutive endpoint failures open a shared 30-second circuit, so cards using one dead origin stop rediscovering the outage. Timeouts, network errors, non-2xx responses, invalid envelopes, and an open circuit all remain on fallback. A host can use useUIResponseSlot() directly for custom placement chrome while keeping the same semantics.

Props

| Prop | Default | What it does | |---|---|---| | fallback | — | Required. Your current component. Paints immediately; stands on text, empty, timeout, error, or open circuit. Omitting it throws. | | id | — | Slot identity. Part of the snapshot/pin key together with ask and context. | | ask | — | The question. View-shaped asks ("show every artist as a table…") verdict ui; open judgment-shaped asks often verdict text. | | ui | "auto" | "auto" \| "always" \| "never". auto lets the router choose prose over a view — and prose means your fallback stands. always forces a view. | | timeoutMs | 2500 | Time-to-verdict. A cold generation takes 20s+, so the default fails closed to the fallback on a first fill. Raise it, or accept snapshot-only painting. | | onStatus | — | (state) => void. The only way to see why a slot didn't fill — the slot logs nothing. Gives status, kind, error, upgrading, refreshed, pinned. | | context | {} | Extra scoping for the ask (e.g. { artist_id }). Part of the snapshot key. Not a credential channel. | | rendererProps | {} | Passed to the renderer. rendererProps.params is forwarded into every POST /v1/resolve body — this is where a per-user JWT goes; see Per-user data and RLS. | | headers | {} | Extra HTTP headers on the fill request — tenant auth for the endpoint (Bearer <your UIR token>). Not the per-user RLS channel. | | endpoint | "" | Origin serving the slot route. | | path | /v1/tenants/demo/ui | Override the slot route (note the demo tenant in the default). | | lifetime | "ephemeral" | ephemeral renders inline. | | chrome | true | false removes all SDK chrome (including the Pin control) and keeps the semantics. |

Why is my slot still showing the fallback?

Usually because it is working as specified, not because it broke:

  • the router verdicted text (open, judgment-shaped ask + default ui="auto") — prose is your fallback. Ask for something view-shaped, or set ui="always";
  • the fill took longer than timeoutMs (the 2.5s default vs a 20s+ cold generation);
  • the endpoint is unreachable, or the circuit is open.

The slot is silent by design — a failed fill logs nothing — so onStatus is your debugger:

<UIResponseSlot {...slotProps} onStatus={(s) => console.log(s.status, s.kind, s.error)} />
// loading → ready + kind:"ui"     ← filled
// loading → ready + kind:"text"   ← verdict was text; the fallback correctly stands

TypeScript

This package ships no type declarations today. tsc rejects the import with TS7016, so npm run build fails in a typechecked app (vite dev is fine — it doesn't typecheck). Until types ship, declare the modules yourself:

// src/uiresponse.d.ts
declare module "@uiresponse/slot-sdk";
declare module "@uiresponse/renderer-react";
declare module "@uiresponse/slot-sdk/styles.css";

Per-user data and RLS

When rows sit behind row-level security, the resolver must act as the signed-in user. The user's JWT rides in the resolve body — never in a header, never in a URL:

<UIResponseSlot
  {...slotProps}
  rendererProps={{ params: { __postgrest_user_jwt: session.access_token } }}
/>

rendererProps.params is forwarded verbatim into every POST /v1/resolve. The PostgREST connector reads params.__postgrest_user_jwt and sends it as the Authorization: Bearer to your PostgREST, alongside the publishable anon key — so RLS evaluates as that user, and this SDK holds no credential that could over-read on its own. The server never logs a resolve body.

Do not reach for headers here: that is tenant auth for the endpoint, a different axis. And UIR_POSTGREST_ANON_KEY must be publishable-class — a service-role key bypasses RLS entirely.

The transport: a fill is an AG-UI run

The default transport is the sibling /v1/tenants/:tenant/agui route: one slot fill is ONE AG-UI run (client.runFill()), on a thread that IS the slot identity. The verdict arrives as a CUSTOM uir.slot_envelope event — the deadline above governs time-to-verdict on the run connection — a stored answer paints as an immediate STATE_SNAPSHOT, and the live upgrade arrives as the same run's events, ending in RUN_FINISHED. A failed upgrade after a painted snapshot is RUN_ERROR: the snapshot stays on screen, the failure counts against the circuit. The JSON sidecar (client.fill(), POST /v1/ui) remains for compatibility — documented legacy, same envelope metadata, same shared stores, no run semantics.

Snapshot→live

A slot answer paints INSTANTLY from the server's last snapshot for {slot id, ask, context} — the envelope arrives with snapshot: true, upgrading: true — and the hook immediately runs the live leg (refresh: true, same circuit breaker). The live answer swaps in place; the state exposes upgrading while the snapshot is showing and refreshed once the live answer landed, and the default <UIResponseSlot> chrome renders those as a quiet mono stamp. Blur-up at envelope scale. Failure semantics are unchanged: if the live leg fails, the (validated) snapshot simply stays; a snapshot never stands in for the host fallback on text/empty verdicts — the live router's verdict always wins, even when it downgrades a snapshot's ui to text.

Pin

useUIResponseSlot() returns pin(): it promotes the answer on screen to a SAVED view server-side (POST /v1/slots/pin — the same validator gate as saving a page) and remembers the slot→view association, so subsequent fills of that exact key serve the saved view via the open_page path instead of regenerating. State exposes pinned/pinning/pinError; the envelope of a pin-served fill carries pinned: true. <UIResponseSlot> shows a minimal Pin control in its chrome; pass chrome={false} to remove all SDK chrome and keep the semantics.

unpin() is the inverse on the same deterministic key (POST /v1/slots/unpin): it removes the slot→view association — subsequent fills stop taking the saved view's path — but the saved view itself stays saved; unpinning a placement never destroys an artifact. The chrome's Pin control is a toggle. Server-side, snapshots also carry an honest freshness posture: a week's TTL and a 500-entry cap by default (UIR_SNAPSHOT_TTL_MS / UIR_SNAPSHOT_MAX_ENTRIES), expired snapshots losing to a live generation instead of racing it.

Three-slot demo

The runnable Vite demo uses the real /v1/tenants/:tenant/ui and /v1/resolve request shapes with a local middleware sidecar. It places slots in an empty state, a sidebar insight, and an inline dashboard row. Together they exercise text, ui, and empty; the sidebar maps metric, table, and badge to host-native components. A fourth panel walks one dead origin through three 2.5-second timeouts and shows the shared circuit bypassing the next attempt.

# prereq, once, from uir/ — this package has no install step of its own; it resolves
# vite/react and the built @uiresponse/web dist through the workspace root:
npm install && npm run build && npm run build:embed

cd uir/packages/slot-sdk
npm run demo

Open the URL Vite prints (normally http://localhost:5173). The mock sidecar is demo-only; change endpoint and/or path on each <UIResponseSlot> to point the same app at a real sidecar.

Host-native component registry

<UIResponseProvider> maps validated semantic block types to host components. It changes painting, not the document or its data semantics:

function TheirStatCard({ block, value, loading, error, actions }) {
  if (loading) return <TheirSkeleton />;
  if (error) return <TheirError message={error.message} />;
  const click = actions.onClick();
  return <TheirCard label={block.label} value={value} disabled={click?.disabled} onClick={click?.run} />;
}

<UIResponseProvider components={{ metric: TheirStatCard, table: TheirDataTable }}>
  <UIResponseSlot {...slotProps} />
</UIResponseProvider>

Every mapping receives adapter contract 1.0 with exactly:

  • adapterVersion and the validated block declaration;
  • resolved value, rows, and columns;
  • loading and error state;
  • actions, whose callbacks re-check the renderer action gate before executing; and
  • children for mapped container blocks (section, card, and tabs).

actions.onClick(row?), rowAction(row), itemAction(row), and onChange(row?) return either null or { declaration, disabled, reason, run }. actions.rowActions(row) adapts every declared table row action, and actions.invoke(declaration, row?) handles another schema-owned action. The adapter never exposes a connector, query, credential, raw renderer context, or arbitrary React. An input can pass its control value with run({ self: nextValue }); a repeating component can override row scope with run({ row }).

An unmapped block is rendered by the first-party renderer. If a mapped host component throws while rendering, its error is contained at that block boundary and that block switches to the first-party renderer; siblings and the surrounding host app remain mounted. rendererProps.onHostComponentError can observe these contained exceptions.

Host-native components, in depth

The goal: a generated view should compose out of your elements — the same hero, the same compact metric cards, the same table density your own pages already use — rather than arriving as a second component library inside your app.

Component Registry

The provider maps UIR's closed semantic block vocabulary onto the host application's own components:

<UIResponseProvider
  components={{
    metric: TheirStatCard,
    table: TheirDataTable,
    badge: TheirStatusChip,
    section: TheirSection,
  }}
>
  <UIResponseSlot {...slotProps} />
</UIResponseProvider>

This is a headless-renderer pattern: semantics are ours; skin is theirs. A generated metric stays a validated metric with UIR's binding, formatting, empty-state, intent, and action rules, but the registry selects the component that paints it. Registry components receive a normalized, versioned adapter contract—not raw internal renderer context—containing the block declaration, resolved value/rows/columns, loading and error state, and gated action callbacks. Missing mappings fall back to UIR's first-party component. A host-component exception is contained at the block boundary and falls back there too; it must never take down the slot or surrounding app.

Container mappings (section, card, tabs) and leaf mappings (metric, table, and so on) use the same registry, but the schema remains the authority on legal composition. The registry cannot introduce arbitrary generated React, bypass validation, read connector credentials, or change data semantics. It is a renderer adapter, not a component-generation escape hatch.

Design catalog (not yet available)

The component registry above is the runtime half, and it ships today. The generation half — a schema-only design catalog, the mirror of UIR's data catalog — is not implemented yet; it is described here so the direction is legible, not because you can use it:

  1. A local tool introspects approved component metadata and representative page composition—types, Storybook controls, design tokens, allowed variants, common parent/child patterns, density, and responsive idioms.
  2. The developer reviews a generated catalog file and explicitly pushes it to UIR Cloud.
  3. Cloud generation sees names and structural idioms such as “product detail pages open with a hero, then use compact metric cards and this table density.” It never receives source code, rendered screenshots, proprietary page content, row data, or credentials.
  4. The generated view still contains only UIR block semantics. At runtime those block types are resolved through the Component Registry, so the result composes like the host app and paints in its actual design system.

Catalog versions should be content-addressed and echoed in generation envelopes for drift checks, exactly as the data catalog is planned to do for schema. Local linting should flag mappings or design idioms that disappeared before a generated view reaches production.