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

@alis-build/a2ui-vuetify-renderer

v0.4.1

Published

A2UI Vuetify Renderer

Readme

A2UI Vuetify Renderer

npm License

A Vue 3 rendering library for the A2UI Protocol v0.9. It translates A2UI JSON messages into interactive Vue interfaces backed by Vuetify 4 components.

The default component catalog extends the ideas of the v0.9 basic catalog with additional Vuetify-backed types (for example Tabs, Modal, Video, AudioPlayer, ChoicePicker). Agents must use the same catalogId your app advertises—typically CATALOG_ID from this package—so createSurface and the renderer registry stay aligned.

How it works

An AI agent produces a stream of JSONL messages (createSurface, updateComponents, updateDataModel, deleteSurface). The renderer consumes these messages through @a2ui/web_core, which handles protocol parsing, state management, and data binding. This library provides the Vue component layer that turns that state into a live Vuetify UI.

Agent → JSONL stream → @a2ui/web_core (MessageProcessor) → Vue reactivity bridge → Vuetify components

Installation

pnpm add @alis-build/a2ui-vuetify-renderer @a2ui/web_core vuetify vue

The library ships ESM (dist/a2ui-vuetify-renderer.js), UMD (dist/a2ui-vuetify-renderer.umd.cjs), and TypeScript declarations (dist/index.d.ts).

Peer dependencies

| Package | Version | | ---------------- | --------- | | vue | ^3.5.35 | | vuetify | ^4.1.1 | | @a2ui/web_core | ^0.10.7 |

Catalog ID

CATALOG_ID is the URI of the Vuetify catalog JSON schema (https://raw.githubusercontent.com/alis-exchange/a2ui-vuetify-renderer/main/catalog/vuetify-catalog.json). That URI always resolves to the catalog on main, so it moves independently of the version you install; for a copy pinned to your installed version read VUETIFY_CATALOG or the ./catalog subpath instead of fetching the URL. Pass it to new Catalog(CATALOG_ID, …), createSurface.catalogId, and a2uiClientCapabilities.supportedCatalogIds so validation and rendering agree.

Setup

1. Configure Vuetify in your host app

The renderer expects Vuetify to be installed as a Vue plugin. You also need vite-plugin-vuetify for component auto-imports.

vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vuetify from 'vite-plugin-vuetify'

export default defineConfig({
  plugins: [vue(), vuetify({ autoImport: true })],
})

main.ts

import { createApp } from 'vue'
import App from './App.vue'
import { createVuetify } from 'vuetify'
import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'

const vuetify = createVuetify()
const app = createApp(App)
app.use(vuetify)
app.mount('#app')

2. (Optional) Install the renderer plugin

The A2UiVueRenderer plugin globally registers <A2uiProvider> and <A2uiComponentNode> and populates the default component registry. You can also pass custom component overrides.

import { A2UiVueRenderer } from '@alis-build/a2ui-vuetify-renderer'

app.use(A2UiVueRenderer, {
  components: {
    GoogleMap: MyCustomMapComponent, // extend the catalog
  },
})

If you skip the plugin, import A2UIProvider, ComponentNode, and registerDefaultComponents, then call registerDefaultComponents() once during app setup so defaultRegistry is populated (the plugin does this inside install).

Quick start

<script setup lang="ts">
  import { ref, onMounted } from 'vue'
  import { MessageProcessor, Catalog, A2uiValidationError } from '@a2ui/web_core/v0_9'
  import type { A2uiClientAction } from '@a2ui/web_core/v0_9'
  import {
    A2UIProvider,
    ComponentNode,
    CATALOG_ID,
    VUETIFY_COMPONENTS,
    VUETIFY_THEME_SCHEMA,
    createVuetifyFunctions,
    registerDefaultComponents,
  } from '@alis-build/a2ui-vuetify-renderer'

  registerDefaultComponents()

  const handleAction = (action: A2uiClientAction) => {
    console.log('Action from A2UI:', action)
  }
  const handleError = (error: unknown) => {
    console.error('A2UI surface error:', error) // unknown component types, cyclic references, expression failures
  }

  // createVuetifyFunctions({ locale }) gives locale-aware formatNumber / formatCurrency / pluralize
  const catalog = new Catalog(CATALOG_ID, VUETIFY_COMPONENTS, createVuetifyFunctions({ locale: navigator.language }), VUETIFY_THEME_SCHEMA)
  // Keep web_core objects out of Vue's reactivity: a plain const, markRaw() or shallowRef(). Never ref()/reactive():
  // calls made through the deep proxy bypass web_core's signals and the UI stops updating.
  const processor = new MessageProcessor([catalog], handleAction, { version: 'v0.9' })
  const surfaceId = 'demo-surface'
  const ready = ref(false)

  onMounted(() => {
    processor.processMessages([
      {
        version: 'v0.9',
        createSurface: {
          surfaceId,
          catalogId: CATALOG_ID,
          theme: { primaryColor: '#6200EE' },
        },
      },
    ])

    try {
      processor.processMessages([
        {
          version: 'v0.9',
          updateComponents: {
            surfaceId,
            components: [
              { id: 'root', component: 'Column', children: ['heading', 'greet-btn'] },
              { id: 'heading', component: 'Text', text: 'Hello from A2UI!', variant: 'h4' },
              { id: 'greet-btn', component: 'Button', variant: 'primary', label: 'Click Me', action: { event: { name: 'greet' } } },
            ],
          },
        },
      ])
    } catch (e) {
      // web_core rejects the whole message when any component fails the catalog schema
      if (e instanceof A2uiValidationError) console.error(e.message, e.details)
      throw e
    }

    ready.value = true
  })
</script>

<template>
  <v-app>
    <v-main class="pa-4">
      <A2UIProvider
        v-if="ready"
        :processor="processor"
        :surface-id="surfaceId"
        :on-action="handleAction"
        :on-error="handleError"
      >
        <ComponentNode id="root" />
      </A2UIProvider>
    </v-main>
  </v-app>
</template>

Architecture

A2UIProvider props

| Prop | Type | Default | Purpose | | --------------- | -------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------- | | processor | MessageProcessor | required | Owns the surface, component and data models. Pass the plain instance (or markRaw / shallowRef), never a ref()/reactive() proxy | | surface-id | string | required | The surface to render | | on-action | (action) => void | – | Fallback handler for component actions | | on-error | (error) => void | console.error | Surface errors reported by web_core (UNKNOWN_COMPONENT_TYPE, CYCLIC_REFERENCE, expression failures) | | node-resolver | boolean | true | Render from web_core's live node tree. false is the static legacy path, which only reflects the surface as of the first paint |

Core modules

| Module | Role | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A2UIProvider | Wraps a surface. Provides processor, surface ID, and action callback via provide/inject. Owns one web_core NodeResolver for the surface (created when the surface appears, disposed when it is deleted or the provider unmounts) and forwards surface errors to onError, including a resolver that fails to build, which falls back to the static path instead of taking the provider down. Surface create/delete events are reconciled against the model's own state, since web_core delivers them asynchronously and a stale one would otherwise tear down a live surface. Bridges A2UI theme colors (primaryColor, errorColor, etc.) into a dynamic Vuetify theme via v-theme-provider. | | ComponentNode | Recursive renderer. id="root" picks the resolver's root node; any other id is looked up among the live child nodes in the parent's resolved props. Mirrors the node's props signal into the context (nodeProps) and scopes descendants to the node's data path, so resolveValue is reactive. Children that have not arrived render as [pending: id] and are swapped in place. Types the catalog does not know are reported through onError and rendered from the registry without live bindings. Supports a path prop for scoped data context on the legacy path. | | ComponentRegistry | A Map<string, Component> that maps A2UI type strings (e.g. "Button") to Vue components. Exposes register(), registerAll(), get(), and has(). The singleton defaultRegistry is pre-populated by registerDefaultComponents(). | | useA2UI() | Composable that injects the provider context. Returns resolveValue<V>(value), resolveDynamicChildren, sendAction, dispatchNodeAction, setData, surfaceId, dataContext, and dataContextPath. resolveValue accepts an A2UI DynamicValue \| undefined (from @a2ui/web_core/v0_9) and resolves via DataContext.resolveDynamicValue<V>(); pass a type argument (e.g. resolveValue<string>(…)) so callers get a typed result. dispatchNodeAction expects a ComponentModel. Uses SurfaceModel.dispatchAction() for schema-validated action payloads. | | useChecks() | Composable bridging a node's checks to Vue: rules for Vuetify inputs, plus isValid / validationErrors as reported by web_core's binder in node mode (Button and IconButton gate on them, Slider and DatePicker display them). errorMessages(value) falls back to the rules whenever the binder reports no failures (the legacy path, and node-mode checks the binder evaluates as passing). | | useDynamicProps() | Composable for custom catalog components: given a node (ref, getter, or plain object), returns a computed ref of properties with each value passed through resolveValue (same generic resolver as useA2UI). Resolved values are still typed as Record<string, any> at the top level; use resolveValue<V>() directly when you need strong typing per field. | | getCatalogSchema() | Returns a deep-cloned JSON Schema for the Vuetify catalog, merging in stub entries for any extra components registered on a ComponentRegistry under the same catalogId (useful for agents or tooling). Accepts an optional { filter } predicate to narrow the returned components — see Filtering the catalog schema below. | | VUETIFY_CATALOG | The generated catalog document itself, inlined by the bundler at no extra cost. Use it when you want the schema as published (feeding an agent library, emitting it from a build step, inspecting the declared components and functions); use getCatalogSchema() instead when runtime-registered components must be merged in. It is the module's shared instance, so treat it as read-only. Outside a bundler the same file ships at the ./catalog subpath: require('@alis-build/a2ui-vuetify-renderer/catalog'). | | catalogFilters | Pre-built filter predicates for getCatalogSchema: catalogFilters.customOnly (non-built-in components only), catalogFilters.only(...names) (include-list), catalogFilters.exclude(...names) (exclude-list). |

Form components map A2UI checks ([{ condition: DynamicBoolean, message }]) to Vuetify validation rules via createVuetifyRules(checks, resolveValue) in src/utils/validation.ts. Each condition (a literal, { path } or { call, args }) is resolved through the surface DataContext every time a rule runs, so it sees the live data model.

Data flow

  1. Messages in — MessageProcessor.processMessages() parses JSONL and updates SurfaceModel state (components, data model, theme). Since @a2ui/web_core 0.10.6 every component in an updateComponents message is validated against the catalog's Zod schema before any state changes: one invalid component throws A2uiValidationError (code: 'VALIDATION_ERROR', details = Zod issues) and rejects the whole message. All Vuetify schemas are strict, so unknown properties are errors; unknown component types are skipped. Catch the error in your transport layer (formatZodIssue from web_core renders single issues).
  2. Reactivity bridge — A2UIProvider owns a web_core NodeResolver; each ComponentNode mirrors its node's props signal into a shallowRef that resolveValue and resolveDynamicChildren read. Server messages and local setData() writes both re-render the affected components without remounting. The binder also evaluates checks and reports isValid / validationErrors on the node; read them through useChecks() — Button and IconButton use them to disable themselves, and Slider and DatePicker to display the failing messages.
  3. Tree resolution — ComponentNode reads the flat adjacency list from SurfaceComponentsModel, resolves children/child/trigger/content references, and recursively renders the tree.
  4. Value binding — Components call resolveValue<V>(value) which delegates to DataContext.resolveDynamicValue<V>() — handling literals, { path } lookups, and { call } function expressions. Prefer an explicit V (e.g. string, number[]) for each property.
  5. Two-way binding — Input components use writable computed properties that call setData() on the surface's DataModel when the user types.
  6. Actions out — On user interaction, components call dispatchNodeAction(node, extraContext?) (reads action from the node, then dispatches) or sendAction(name, sourceComponentId, context) (fires a named event directly). Both resolve context from the data model and dispatch via SurfaceModel.dispatchAction() or the onAction callback.

Dynamic list rendering

When children is an object { path, componentId } instead of an array, the renderer iterates the data model array at path and renders the template componentId for each item, passing a scoped path prop (/items/0, /items/1, ...) so child components resolve relative bindings correctly.

Custom Components

You can extend the renderer by defining your own Vue components and registering them. This is useful for complex data visualizations, specialized input fields, or integrating third-party libraries.

1. Define a Custom Component

Use the useA2UI composable to easily map A2UI JSON properties to your component's props and emit actions back to the agent.

<!-- src/components/CustomChartWidget.vue -->
<template>
  <v-card variant="outlined" class="pa-4 border-info">
    <div class="text-h6 mb-2">{{ title }}</div>
    <div class="d-flex align-end" style="height: 100px; gap: 8px;">
      <div 
        v-for="(point, idx) in chartData" 
        :key="idx"
        class="bg-info rounded-t cursor-pointer"
        :style="{ height: `${point}%`, width: '40px', transition: 'height 0.3s' }"
        @click="handleBarClick(idx, point)"
      ></div>
    </div>
  </v-card>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { useA2UI, type ComponentModel } from '@alis-build/a2ui-vuetify-renderer';

const props = defineProps<{
  node: ComponentModel;
}>();

// useA2UI automatically handles resolving dynamic data bindings (e.g. { path: '/myData' })
const { resolveValue, dispatchNodeAction } = useA2UI();

// Resolve the 'title' property from the A2UI JSON node (generic = resolved type)
const title = computed(() => resolveValue<string | undefined>(props.node.properties.title) ?? 'Default Chart');

// Resolve the 'data' property from the A2UI JSON node, defaulting to an empty array
const chartData = computed(() => {
  const data = resolveValue<number[] | undefined>(props.node.properties.data);
  return Array.isArray(data) ? data : [];
});

// Dispatch the action configured on the node (see ComponentApi below) with extra context
const handleBarClick = (index: number, value: number) => {
  dispatchNodeAction(props.node, { index, value });
};
</script>

2. Register the Component

Before rendering a surface, register your new component with the defaultRegistry using the same CATALOG_ID that your surface uses.

You can optionally provide a ComponentApi definition for your component. This is highly recommended as it allows getCatalogSchema() to expose your custom component's properties to the LLM agent, so the agent knows exactly what data your component expects.

Include ActionSchema in the schema when your component emits user interactions back to the agent. The agent chooses the event name in JSON; your component decides when to fire it and what extra context to attach.

import { CATALOG_ID, defaultRegistry } from '@alis-build/a2ui-vuetify-renderer';
import { ActionSchema, type ComponentApi } from '@a2ui/web_core/v0_9';
import { z } from 'zod';
import CustomChartWidget from './components/CustomChartWidget.vue';

// Define the ComponentApi using Zod for your custom component
const CustomChartApi: ComponentApi = {
  name: 'CustomChart',
  schema: z.object({
    title: z.string().describe('The title of the chart').optional(),
    data: z.object({ path: z.string() }).describe('Path to the data model array'),
    action: ActionSchema.optional().describe('Fired when a bar is clicked; context includes index and value'),
  }).strict(),
};

// Register the component under the type name "CustomChart" along with its API
defaultRegistry.register(CATALOG_ID, 'CustomChart', CustomChartWidget, CustomChartApi);

Use ActionSchema (required) when the component only makes sense with an action — e.g. Button. Use ActionSchema.optional() when interaction is optional — e.g. TextField, Calendar.

Action shape

ActionSchema from @a2ui/web_core/v0_9 accepts either:

  • Server action — { event: { name: string, context?: object } } — dispatched to the agent via onAction
  • Client function — { functionCall: { call: string, args?: object } } — evaluated locally by the renderer

In your Vue component, call dispatchNodeAction(node, extraContext?) on user interaction. It reads node.properties.action, merges extraContext into the event context, and dispatches. Built-in components such as Button and Calendar follow this same pattern.

dispatchNodeAction vs sendAction

Both functions emit actions back to the agent (or execute client-side functionCall actions). dispatchNodeAction is a higher-level helper that reads the node's action property first; for server events it delegates to sendAction.

| | dispatchNodeAction(node, extraContext?) | sendAction(name, sourceComponentId, context?) | | --- | --- | --- | | Who sets the event name | The agent, via action.event.name in JSON | Your component code (first argument) | | Static context | From action.event.context in JSON (literals, { path }, { call } resolved) plus extraContext you pass at runtime | Only the context argument you pass | | functionCall actions | Yes — calls dataContext.resolveAction() | No — server events only | | Visible in catalog schema | Yes, when action: ActionSchema is in ComponentApi | No, unless the schema also declares action | | When nothing happens | Silently returns if node.properties.action is unset | Always dispatches when called |

Prefer dispatchNodeAction for agent-driven UIs. The agent supplies action in updateComponents JSON; your component decides when to fire it and what runtime fields to add:

// Agent JSON: { "action": { "event": { "name": "chartPointClicked" } } }
dispatchNodeAction(props.node, { index, value });

Use sendAction when the event name is fixed in code and does not come from the node's action property — for example, CustomChartWidget in examples/client:

sendAction('chartPointClicked', props.node.id, { index, value });

This works at runtime, but the agent will not see that event in getCatalogSchema() unless you also add action: ActionSchema and switch to dispatchNodeAction. Built-in components such as Button use dispatchNodeAction; see A2UIButton.vue and A2UICalendar.vue for reference.

Multiple actions on one component

A2UI defines one standard action property per component node. dispatchNodeAction only reads node.properties.action. When a component has more than one user interaction, pick the pattern that fits:

| Goal | Pattern | | --- | --- | | Several gestures, one handler (agent branches on context) | One action + context discriminator | | Different event names, all on the same custom node | Multiple ActionSchema properties (custom) | | Separate affordances (Confirm / Cancel buttons) | Child Button components (standard A2UI) | | Fixed event names, not agent-configured | Multiple sendAction calls |

1. One action, discriminate in context (recommended)

Use a single action on the node. Your component tags which interaction fired via extraContext. The agent (or your onAction handler) branches on a field such as interaction.

Calendar is the built-in example. Its schema declares one optional action (A2UICalendar.api.ts); A2UICalendar.vue dispatches the same action for two gestures with different context:

// A2UICalendar.api.ts — one action property
action: ActionSchema.optional(),

// Agent JSON — agent picks the event name once
{
  "id": "my-calendar",
  "component": "Calendar",
  "events": [{ "name": "Team standup", "start": "2026-06-09" }],
  "value": { "path": "/selectedDate" },
  "action": { "event": { "name": "calendarInteraction" } }
}

// A2UICalendar.vue — discriminate by interaction
const handleClickDate = (_e: Event, day: any) => {
  const date = day?.date;
  if (date) modelValue.value = date;
  dispatchNodeAction(props.node, { date, interaction: 'click:date' });
};

const handleClickEvent = (_e: Event, eventScope: any) => {
  const event = eventScope?.event;
  dispatchNodeAction(props.node, {
    event: event ? { name: event.name, start: event.start, end: event.end } : {},
    interaction: 'click:event',
  });
};

Tabs uses the same pattern for tab changes — one action, context carries tabIndex and tabTitle (A2UITabs.vue).

2. Multiple ActionSchema properties (custom)

No built-in component in this catalog uses multiple ActionSchema properties on one node. If you extended Calendar so date clicks and event clicks each needed a separate agent-configurable event name (instead of one action + interaction), you could add custom properties to your ComponentApi:

// Hypothetical extension of Calendar — not in the default catalog
const ExtendedCalendarApi: ComponentApi = {
  name: 'ExtendedCalendar',
  schema: z.object({
    events: z.array(z.object({ name: z.string(), start: z.string() }).strict()),
    value: DynamicStringSchema.optional(),
    onDateClickAction: ActionSchema.optional().describe('Fired when a date cell is clicked'),
    onEventClickAction: ActionSchema.optional().describe('Fired when a calendar event is clicked'),
  }).strict(),
};
{
  "onDateClickAction": { "event": { "name": "dateSelected" } },
  "onEventClickAction": { "event": { "name": "eventSelected" } }
}

dispatchNodeAction only reads node.properties.action, so resolve and dispatch each property yourself:

import type { Action } from '@a2ui/web_core/v0_9';

const { resolveValue, sendAction } = useA2UI();

function dispatchConfiguredAction(
  actionValue: unknown,
  extraContext?: Record<string, unknown>,
) {
  const action = resolveValue<Action | undefined>(actionValue);
  if (!action || !('event' in action)) return;

  sendAction(action.event.name, props.node.id, {
    ...(action.event.context ?? {}),
    ...extraContext,
  });
}

// Mirrors A2UICalendar.vue handlers, but reads separate action properties
function handleClickDate(_e: Event, day: { date?: string }) {
  dispatchConfiguredAction(props.node.properties.onDateClickAction, {
    date: day?.date,
    interaction: 'click:date',
  });
}

function handleClickEvent(_e: Event, eventScope: { event?: { name: string; start: string; end?: string } }) {
  const event = eventScope?.event;
  dispatchConfiguredAction(props.node.properties.onEventClickAction, {
    event: event ? { name: event.name, start: event.start, end: event.end } : {},
    interaction: 'click:event',
  });
}

Prefer pattern 1 unless you truly need distinct event names per gesture on the same node.

3. Child Button components (standard A2UI)

For discrete actions (Save, Close, Submit), put each action on a child Button with its own action property. Modal composes child nodes via trigger and content (A2UIModal.vue); buttons inside the modal body each carry a standard action:

[
  { "id": "settings-modal", "component": "Modal", "trigger": "open-settings-btn", "content": "settings-modal-body" },
  { "id": "open-settings-btn", "component": "Button", "label": "Settings", "variant": "primary" },
  {
    "id": "settings-modal-body",
    "component": "Column",
    "children": ["settings-form", "save-settings-btn", "close-settings-btn"]
  },
  { "id": "save-settings-btn", "component": "Button", "label": "Save", "variant": "primary", "action": { "event": { "name": "saveSettings" } } },
  { "id": "close-settings-btn", "component": "Button", "label": "Close", "action": { "event": { "name": "closeSettingsModal" } } }
]

Modal itself can also emit on open/close via its optional action (A2UIModal.vue watches isOpen and calls dispatchNodeAction with { open }). Put form state in the data model so Save's action.event.context can reference it (e.g. { "settings": { "path": "/settings" } }).

4. Multiple sendAction calls (fixed names)

When event names are fixed in code and do not need agent configuration. CustomChartWidget (examples/client) uses this for bar clicks:

sendAction('chartPointClicked', props.node.id, { index, value });

Additional handlers on the same component would follow the same pattern with different names. Event names are invisible to getCatalogSchema() unless you add action: ActionSchema and migrate to dispatchNodeAction (patterns 1 or 2).

3. Send it from the Agent

Now your agent can send updateComponents messages using your new "CustomChart" type.

{
  "id": "my-custom-chart",
  "component": "CustomChart",
  "title": "Monthly Sales",
  "data": { "path": "/salesData" },
  "action": { "event": { "name": "chartPointClicked" } }
}

The agent sets action.event.name; your component attaches runtime context (e.g. index, value) via dispatchNodeAction. Optional static context can be included in action.event.context using literals or data bindings:

{
  "action": {
    "event": {
      "name": "chartPointClicked",
      "context": { "chartId": { "path": "/activeChartId" } }
    }
  }
}

Filtering the catalog schema

getCatalogSchema() accepts an optional filter predicate to control which components appear in the returned schema. This is useful when you only want to expose a subset of components to the LLM agent.

import { getCatalogSchema, catalogFilters, defaultRegistry, CATALOG_ID } from '@alis-build/a2ui-vuetify-renderer';

// Full catalog (default — same as before)
const full = getCatalogSchema(defaultRegistry, CATALOG_ID);

// Only custom (non-built-in) components
const custom = getCatalogSchema(defaultRegistry, CATALOG_ID, {
  filter: catalogFilters.customOnly,
});

// Only specific components
const subset = getCatalogSchema(defaultRegistry, CATALOG_ID, {
  filter: catalogFilters.only('Button', 'TextField', 'Card'),
});

// Everything except a few
const lighter = getCatalogSchema(defaultRegistry, CATALOG_ID, {
  filter: catalogFilters.exclude('Table', 'Calendar'),
});

// Arbitrary predicate
const myFilter = getCatalogSchema(defaultRegistry, CATALOG_ID, {
  filter: (name) => name.startsWith('Custom'),
});

| Helper | Description | | --- | --- | | catalogFilters.customOnly | Keeps only components not in the base Vuetify catalog | | catalogFilters.only(...names) | Keeps only the listed component names | | catalogFilters.exclude(...names) | Keeps everything except the listed component names |

Component catalog

40 components are registered in the default catalog. Most are backed by Vuetify 4; media types use native HTML5 elements where that fits the protocol.

Content & display

| A2UI type | Vuetify / HTML | Notes | | ------------ | ---------------------------- | ---------------------------------------------------------------- | | Text | <div> + typography classes | Maps variant (h1–h5, body, caption) to text-h1…text-body-1 | | Image | v-img | url, fit, variant | | Icon | v-icon | Material Design Icons | | Divider | v-divider | Horizontal/vertical axis | | Avatar | v-avatar | | | Badge | v-badge | | | Chip | v-chip | | | Alert | v-alert | | | Banner | v-banner | | | EmptyState | v-empty-state | | | Rating | v-rating | | | Video | <video> | url, controls, autoplay, loop, muted | | AudioPlayer | <audio> | Same media-style props as Video |

Layout & containers

| A2UI type | Vuetify component | Notes | | ---------------- | ---------------------------------- | ---------------------------------------------------------- | | Row | <div class="d-flex flex-row"> | justify, align mapped to Vuetify flex utility classes | | Column | <div class="d-flex flex-column"> | Same flex mapping; child weight → flex-grow-N | | List | v-list | Supports dynamic { path, componentId } children | | Card | v-card | Single child reference | | Form | v-form | Wraps children | | ExpansionPanel | v-expansion-panels | | | Tabs | v-tabs / v-tabs-window | Tab titles and panel content per A2UI tabs array | | Modal | v-dialog | trigger opens dialog; content is body | | Table | v-data-table | Auto-generates headers from data if columns not provided | | TreeView | v-treeview | | | Calendar | v-calendar | |

Interactive & forms

| A2UI type | Vuetify component | Notes | | -------------- | ---------------------------- | --------------------------------------------------------------------------------------------------- | | Button | v-btn | variant → elevated/tonal/text; action.event triggers dispatchNodeAction → sendAction; action.functionCall runs locally; disabled while its checks fail | | TextField | v-text-field | Two-way binding via value.path; checks ({ condition, message }) → Vuetify rules | | TextArea | v-textarea | Same binding pattern | | NumberInput | v-text-field type="number" | | | Checkbox | v-checkbox | | | RadioButton | v-radio-group / v-radio | | | Select | v-select | variant: "multipleSelection" → multiple prop | | Autocomplete | v-autocomplete | | | Combobox | v-combobox | | | FileInput | v-file-input | | | Slider | v-slider | | | RangeSlider | v-range-slider | | | DatePicker | v-date-picker | | | TimePicker | v-time-picker | | | ChoicePicker | (delegates) | Maps A2UI choice semantics to Select / RadioButton / Checkbox based on variant and display style |

Theming

A2UIProvider reads the theme object from createSurface and dynamically registers a Vuetify theme scoped to that surface. Supported theme properties:

  • primaryColor → Vuetify primary
  • errorColor → Vuetify error
  • backgroundColor → Vuetify background
  • surfaceColor → Vuetify surface

The theme is applied via <v-theme-provider> and automatically cleaned up when the provider unmounts.

Custom components

Registering & Scoping (catalogId)

To avoid collisions between disparate models, components must be registered against a specific catalogId. The renderer uses this ID (provided via createSurface) to securely sandbox standard and custom components.

import { CATALOG_ID, defaultRegistry } from '@alis-build/a2ui-vuetify-renderer'
import MyMap from './components/MyMap.vue'

// Registering a synchronous custom component under the default catalog ID
defaultRegistry.register(CATALOG_ID, 'GoogleMap', MyMap)

Lazy Loading

To optimize your initial bundle, you can register components asynchronously. Vue will load the chunk only when the A2UI server requests the component.

import { defineAsyncComponent } from 'vue'

const AsyncMap = defineAsyncComponent(() => import('./components/MyMap.vue'))

defaultRegistry.register(CATALOG_ID, 'GoogleMap', AsyncMap)

Transport Integration

While the A2UI Vue Renderer focuses solely on the UI layer, host applications must integrate it into an overarching transport channel (e.g., SSE, WebSockets, or a native bridge) and announce their capabilities.

Client Capabilities

You must explicitly announce which catalogIds your renderer supports to the AI agent during the connection handshake. Failure to do so may result in the server sending unhandled component types.

// The processor derives the capabilities from its catalogs
const metadata = processor.getClientCapabilities()
// { 'v0.9': { supportedCatalogIds: [CATALOG_ID] } }

// Advertise protocol v0.9.1 instead (messages with version 'v0.9.1' are accepted either way),
// or embed the full catalog schemas for agents that cannot fetch CATALOG_ID
const metadata091 = processor.getClientCapabilities({ version: 'v0.9.1', includeInlineCatalogs: true })
// Send metadata to the server...

MessageProcessor takes the default version as its third argument: new MessageProcessor([catalog], handleAction, { version: 'v0.9.1' }).

Authoring

Custom components receive a node prop (the component node from A2UI). To resolve data bindings (path, call, literals) for every field, use useDynamicProps with a ref, getter, or the node object (it uses Vue’s toValue internally). Each entry is resolved with the same resolveValue helper as useA2UI; for strongly typed fields, call resolveValue<V>(props.node.properties.someKey) instead of reading only from dynamicProps.

<script setup lang="ts">
  import { toRef } from 'vue'
  import { useDynamicProps, useA2UI, type ComponentModel } from '@alis-build/a2ui-vuetify-renderer'

  const props = defineProps<{ node: ComponentModel }>()

  const dynamicProps = useDynamicProps(toRef(props, 'node'))

  const { dispatchNodeAction, sendAction, setData } = useA2UI()

  function handleClick(coords: { lat: number; lng: number }) {
    setData('/map/lastClick', coords)
    // Prefer dispatchNodeAction when the agent sets action on the node (see Custom Components above)
    dispatchNodeAction(props.node, coords)
    // Or sendAction for a fixed event name not configured in JSON (see CustomChartWidget):
    // sendAction('chartPointClicked', props.node.id, { index, value })
  }
</script>

<template>
  <div class="custom-map">
    Latitude: {{ dynamicProps.lat }}
    Longitude: {{ dynamicProps.lng }}
    <v-btn @click="handleClick({ lat: 0, lng: 0 })">Click Map</v-btn>
  </div>
</template>

Catalog functions

createVuetifyFunctions({ locale }) returns the FunctionImplementation[] for the Vuetify catalog: the basic catalog functions (required, regex, formatString, and, …) with two differences:

  • Locale — formatNumber, formatCurrency and pluralize use the given BCP 47 locale (e.g. navigator.language). Omit it for the host runtime locale.
  • openUrl — the basic catalog's openUrl (web_core ≥ 0.10.2) only allows http:/https:. The Vuetify override keeps that protection but also allows mailto: and tel::

| URL scheme | Behaviour | | ------------------------------------------------ | ---------------------------------------------------------------- | | http:, https: (relative URLs resolve first) | window.open(url, '_blank', 'noopener,noreferrer') | | mailto:, tel: | window.location.assign(url) (no blank tab left behind) | | anything else (javascript:, data:, file:…) | throws A2uiExpressionError (Unsupported URL scheme: …) |

Function names, schemas and return types are identical to the basic catalog, so catalog/vuetify-catalog.json does not change. VUETIFY_FUNCTIONS is createVuetifyFunctions() and VuetifyOpenUrlImplementation is exported on its own.

Exports

Everything is available from the package root:

// Components
import { A2UIProvider, ComponentNode } from '@alis-build/a2ui-vuetify-renderer'

// Composables
import { useA2UI, useDynamicProps, useChecks } from '@alis-build/a2ui-vuetify-renderer'

// Registry & catalog bootstrap
import {
  ComponentRegistry,
  defaultRegistry,
  registerDefaultComponents,
  CATALOG_ID,
  VUETIFY_COMPONENTS,
  VUETIFY_FUNCTIONS,
  VUETIFY_THEME_SCHEMA,
  createVuetifyFunctions,
  VuetifyOpenUrlImplementation,
  getCatalogSchema,
  catalogFilters,
} from '@alis-build/a2ui-vuetify-renderer'

// Plugin
import { A2UiVueRenderer } from '@alis-build/a2ui-vuetify-renderer'

// Types
import type { A2UIContext, A2UIActionPayload, A2UiVueRendererOptions, VuetifyFunctionsOptions, UseChecksReturn, ComponentModel } from '@alis-build/a2ui-vuetify-renderer'

// Injection keys (for advanced provide/inject usage)
import { A2UI_CONTEXT_KEY, A2UI_REGISTRY_KEY } from '@alis-build/a2ui-vuetify-renderer'

// Styles (import in your app entry)
import '@alis-build/a2ui-vuetify-renderer/style.css'

Development

git clone https://github.com/alis-exchange/a2ui-vuetify-renderer.git
pnpm install

| Command | Description | | ----------------------- | ---------------------------------------------------------- | | pnpm dev | Start Vite dev server (playground under examples/client) | | pnpm build | Type-check with vue-tsc then build the library | | pnpm test | Run Vitest test suite | | pnpm generate:catalog | Regenerate catalog/vuetify-catalog.json from sources |

Project structure

renderer/
├── catalog/
│   └── vuetify-catalog.json        # JSON Schema + catalogId for agents
├── examples/client/                # Vite app for manual integration testing
├── scripts/
│   └── generate-catalog.ts         # Catalog JSON build script
├── src/
│   ├── index.ts                    # Public API barrel
│   ├── A2UIRendererPlugin.ts       # Vue plugin (install)
│   ├── style.css                   # Base styles
│   ├── catalog/
│   │   ├── index.ts                # Barrel for catalog exports
│   │   ├── vuetify-components.ts   # Zod-based ComponentApi[] for all Vuetify components
│   │   ├── vuetify-functions.ts    # createVuetifyFunctions(): basic catalog + Vuetify openUrl
│   │   └── vuetify-theme.ts        # Zod theme schema for Catalog constructor
│   ├── composables/
│   │   ├── A2UIProvider.vue        # Context provider + theme bridge
│   │   ├── useA2UI.ts              # Core composable
│   │   └── useDynamicProps.ts      # Resolved props helper for custom widgets
│   ├── core/
│   │   ├── ComponentNode.vue       # Recursive component renderer
│   │   ├── ComponentRegistry.ts    # Type → Component map (per catalogId)
│   │   ├── constants.ts            # CATALOG_ID
│   │   ├── defaultCatalog.ts       # Registers 40 default components
│   │   ├── getCatalogSchema.ts     # Schema merge for tooling / agents
│   │   └── catalogFilters.ts      # Pre-built filter predicates
│   ├── components/
│   │   └── A2UI*.vue               # Vuetify-backed (and media) implementations
│   └── utils/
│       └── validation.ts           # A2UI checks ({ condition, message }) → Vuetify rules
├── dist/                           # Built library output
├── vite.config.ts                  # Vite lib mode + vuetify auto-import
├── vitest.config.ts                # Test config (jsdom, vuetify inlined)
└── tsconfig.json                   # Project references

Testing

Tests use Vitest with @vue/test-utils and jsdom. Vuetify is inlined during tests via vitest.config.ts. Test files live alongside source as *.spec.ts:

  • Core — plugin, provider (real MessageProcessor), useA2UI, useDynamicProps, ComponentNode, liveNodes, ComponentRegistry, getCatalogSchema, barrel index.spec.ts
  • End to end — nodeResolver.spec.ts renders the shipped components through the resolver: typing updates siblings, checks gate the Button, template lists grow on local writes, placeholders upgrade
  • Components — grouped specs (CoreComponents, FormInputs, …) plus focused tests for Tabs, Modal, Video, AudioPlayer, ChoicePicker
  • Tooling — scripts/generate-catalog.spec.ts, validation.spec.ts
  • Catalog — catalog/vuetify-functions.spec.ts (locale, openUrl allowlist) and catalog/catalog.spec.ts (strict message validation under MessageProcessor)

License

Apache 2.0