Chat · A2UI

createA2uiSurfaceStore()

Factory function that creates an A2uiSurfaceStore — a reactive store that accumulates A2UI messages into a live Map of surfaces.

Import:

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

#Signature

function createA2uiSurfaceStore(): A2uiSurfaceStore

Returns: A2uiSurfaceStore — a stateful store backed by Angular signals. Can be created outside an injection context.

#A2uiSurfaceStore Interface

interface A2uiSurfaceStore {
  /** Apply an A2UI message, updating surfaces reactively. */
  apply(message: A2uiMessage): void;
 
  /** Signal containing all current surfaces, keyed by surfaceId. */
  readonly surfaces: Signal<Map<string, A2uiSurface>>;
 
  /** Returns a computed signal for a single surface by ID. */
  surface(surfaceId: string): Signal<A2uiSurface | undefined>;
}

#apply(message)

Processes one A2uiMessage and updates the internal surfaces signal. All four message types are handled:

Message typeBehavior
surfaceUpdateDelivers a surface's components — merges the provided components into the surface's component map by id so existing components are replaced and others are kept
dataModelUpdateApplies typed contents entries to the surface's data model, optionally under a JSON Pointer path (see below)
beginRenderingMarks the surface root and commits the buffered components and data model into a live surface
deleteSurfaceRemoves the surface from the map entirely

Messages for unknown surface IDs are silently ignored until a surfaceUpdate introduces the surface and a beginRendering commits it.

#surfaces

A readonly Signal<Map<string, A2uiSurface>> containing all active surfaces. Each map operation produces a new Map reference so that Angular's change detection triggers correctly.

#surface(surfaceId)

Returns a computed signal for a single surface. The signal emits undefined until a beginRendering message commits it, and undefined again after deleteSurface removes it.

const dashboard = store.surface('dashboard');
// dashboard() is A2uiSurface | undefined

#Data Model and Render-Lib State Sync

The surface's dataModel is synchronized into the render-lib StateStore when surfaceToSpec() converts the surface to a spec. The conversion sets state: surface.dataModel on the produced Spec, which initializes the render-lib's internal StateStore with the surface data. When components with _bindings update values (e.g., a text field changing), those updates flow through the render-lib StateStore, and each mutation emits a RenderStateChangeEvent through the render-lib event system. This means consumers observing the events output on A2uiSurfaceComponent see all data model changes as typed RenderStateChangeEvent objects with path, value, and snapshot fields.

#dataModelUpdate Semantics

The dataModelUpdate message carries a contents array of typed data-model entries. When path is omitted or '/', those entries update the root data model. When path points at a nested object, the entries update that nested object.

Each entry chooses one typed value field: valueString, valueNumber, valueBoolean, or valueMap. valueMap is a nested array of A2uiDataModelEntry objects.

// Update root fields
{
  "dataModelUpdate": {
    "surfaceId": "s1",
    "contents": [
      { "key": "name", "valueString": "Alice" },
      { "key": "score", "valueNumber": 42 }
    ]
  }
}
 
// Update fields under /profile
{
  "dataModelUpdate": {
    "surfaceId": "s1",
    "path": "/profile",
    "contents": [
      { "key": "approved", "valueBoolean": true },
      { "key": "meta", "valueMap": [{ "key": "source", "valueString": "agent" }] }
    ]
  }
}

Current merge semantics are additive. Entries replace values at their keys, but missing keys are left alone. A dataModelUpdate does not delete a key by omitting it.

#Usage with createA2uiMessageParser

The surface store is designed to work with createA2uiMessageParser, which parses raw JSONL chunks into typed A2uiMessage objects.

import { createA2uiSurfaceStore } from '@threadplane/chat';
import { createA2uiMessageParser } from '@threadplane/a2ui';
import { effect } from '@angular/core';
 
const store = createA2uiSurfaceStore();
const parser = createA2uiMessageParser();
 
// Feed raw JSONL chunks as they arrive from the stream
function onChunk(chunk: string): void {
  const messages = parser.push(chunk);
  for (const msg of messages) {
    store.apply(msg);
  }
}
 
// React to surface changes
effect(() => {
  const surface = store.surface('dashboard')();
  if (surface) {
    console.log('Components:', [...surface.components.keys()]);
    console.log('Data model:', surface.dataModel);
  }
});
JSONL envelope format

The parser expects each line to be wrapped in an envelope object: {"surfaceUpdate": {...}}, {"dataModelUpdate": {...}}, etc. The envelope key determines the message type; its value is the message payload.

#A2uiSurface Shape

Each surface stored in the map has the following structure:

interface A2uiSurface {
  surfaceId: string;
  catalogId: string;
  theme?: A2uiTheme;
  components: Map<string, A2uiComponent>;
  dataModel: Record<string, unknown>;
}

The components map is keyed by component ID. The dataModel is a plain object that components reference via JSON Pointer paths in their props.

#What's Next