Chat · A2UI

A2uiSurfaceComponent

Angular component that renders a single A2UI surface. It converts the surface's component map and data model into a json-render Spec, then delegates rendering to RenderSpecComponent.

Import:

import { A2uiSurfaceComponent } from '@threadplane/chat';

Selector: a2ui-surface

#Inputs

InputTypeRequiredDescription
surfaceA2uiSurfaceNo*The surface to render (legacy input shape)
stateA2uiSurfaceStateNo*Chat-side surface state driving progressive rendering. Preferred over surface; takes priority when both are set
catalogA2uiViews | ViewRegistryYesComponent registry used to resolve A2UI type names to Angular components
handlersRecord<string, (params) => unknown | Promise<unknown>>NoConsumer A2UI action handlers for a2ui:localAction. Merged with the built-in a2ui:event / a2ui:localAction handlers; consumer handlers take priority
surfaceFallbackType<unknown> | undefinedNoComponent mounted while no root component has arrived yet (instead of rendering nothing)
Provide one of surface or state

surface and state are both optional individually, but the component needs one of them to render. state is the preferred progressive-rendering path; surface is the legacy shape.

#Outputs

OutputTypeDescription
eventsRenderEventEmits render events from the underlying RenderSpecComponent -- handler execution, state changes, and lifecycle signals
actionA2uiActionMessageEmits an agent-bound action message when an A2UI event action fires. Forward it to your agent (e.g. agent.submit) to resume the agent loop

The events output forwards all RenderEvent emissions from the render engine. This includes events triggered by the default A2UI handlers (a2ui:event and a2ui:localAction) as well as any state change or lifecycle events. The action output is the agent-bound path: when a surface's event action fires, the component builds an A2uiActionMessage and emits it here so you can send it back to the agent.

#How It Works

A2uiSurfaceComponent bridges the A2UI surface model to the json-render engine in three steps:

1. Convert surface to Spec (surfaceToSpec)

The surfaceToSpec() function walks the flat component map and produces a json-render Spec. It requires a component with id: 'root' to be present — if no root exists, nothing renders.

2. Resolve dynamic values

Before the spec is emitted, each component prop is evaluated against the surface's data model. A prop can be:

  • A literal value — passed through as-is
  • A path reference { path: '/some/pointer' } — resolved via JSON Pointer against dataModel
  • A function call { call: 'formatCurrency', args: { ... } } — executed by the built-in function registry
  • A template string "Hello ${/name}" — interpolated with values from dataModel

3. Map actions to on bindings

surfaceToSpec() converts each component's A2UI action prop into a render-spec on binding on the corresponding element. Event actions map to the a2ui:event handler, and function call actions map to the a2ui:localAction handler. This bridges the A2UI interaction model to the render-lib event system.

4. Expand template children

When a component's children field is an A2uiChildTemplate ({ path, componentId }), the surface component expands it over the array at path in the data model. Each array item gets its own cloned element with props resolved in that item's scope.

5. Render via RenderSpecComponent

The final Spec is passed to RenderSpecComponent along with the ViewRegistry (converted to an Angular registry via toRenderRegistry). Rendering is fully reactive — when the surface signal updates, the spec recomputes and only affected elements re-render.

#Usage Outside ChatComponent

A2uiSurfaceComponent is used internally by ChatComponent, but you can also use it directly to embed a surface in any Angular template.

import { Component, signal } from '@angular/core';
import { A2uiSurfaceComponent, createA2uiSurfaceStore, a2uiBasicCatalog } from '@threadplane/chat';
import { createA2uiMessageParser } from '@threadplane/a2ui';
 
@Component({
  selector: 'app-agent-panel',
  standalone: true,
  imports: [A2uiSurfaceComponent],
  template: `
    @if (store.surface('dashboard')() as surface) {
      <a2ui-surface [surface]="surface" [catalog]="catalog" />
    }
  `,
})
export class AgentPanelComponent {
  readonly catalog = a2uiBasicCatalog();
  readonly store = createA2uiSurfaceStore();
  private readonly parser = createA2uiMessageParser();
 
  receiveChunk(chunk: string): void {
    for (const msg of this.parser.push(chunk)) {
      this.store.apply(msg);
    }
  }
}
Root component required

The surface must contain a component with id: 'root'. If no root component has been received yet (for example, while the agent is still streaming), A2uiSurfaceComponent renders nothing until one arrives.

#Wiring handlers and actions

The example above renders a surface but ignores interaction. To run client-owned behavior and forward agent-bound events, pass [handlers] and bind (action) / (events). Consumer handlers run for a2ui:localAction; the (action) output carries the agent-bound A2uiActionMessage.

import { Component, inject, signal } from '@angular/core';
import { Router } from '@angular/router';
import { A2uiSurfaceComponent, a2uiBasicCatalog } from '@threadplane/chat';
import { injectAgent } from '@threadplane/langgraph';
import type { A2uiActionMessage, A2uiSurface } from '@threadplane/chat';
import type { RenderEvent } from '@threadplane/render';
 
@Component({
  selector: 'app-interactive-surface',
  standalone: true,
  imports: [A2uiSurfaceComponent],
  template: `
    <a2ui-surface
      [surface]="surface()"
      [catalog]="catalog"
      [handlers]="handlers"
      (action)="sendToAgent($event)"
      (events)="onEvent($event)"
    />
  `,
})
export class InteractiveSurfaceComponent {
  private readonly router = inject(Router);
  private readonly agent = injectAgent();
 
  readonly catalog = a2uiBasicCatalog();
  readonly surface = signal<A2uiSurface | undefined>(undefined);
 
  // Client-owned local actions (a2ui:localAction).
  readonly handlers = {
    openDetails: async (args: Record<string, unknown>) => {
      await this.router.navigate(['/orders', args['orderId']]);
    },
  };
 
  // Agent-bound action: forward it back to the agent loop.
  sendToAgent(message: A2uiActionMessage) {
    this.agent.submit({ resume: message });
  }
 
  onEvent(event: RenderEvent) {
    console.log('render event', event);
  }
}

#surfaceToSpec()

The conversion function is exported for testing or custom rendering pipelines.

Import:

import { surfaceToSpec } from '@threadplane/chat';

Signature:

function surfaceToSpec(surface: A2uiSurface): Spec | null

Returns null when the surface has no root component. Otherwise returns a complete json-render Spec with all dynamic values resolved against the current dataModel.

#What's Next