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 a JSON Pointer patch to the surface's data model (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 uses JSON Pointer (RFC 6901) paths to address values in the data model.

pathvalueEffect
undefined or '/'ObjectReplaces the entire data model
/some/pathAny valueSets dataModel[some][path] to value
/some/pathundefinedDeletes the value at that path
// Replace entire model
{"dataModelUpdate": {"surfaceId": "s1", "value": {"name": "Alice", "score": 42}}}
 
// Set a single field
{"dataModelUpdate": {"surfaceId": "s1", "path": "/score", "value": 99}}
 
// Delete a field
{"dataModelUpdate": {"surfaceId": "s1", "path": "/score"}}

#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