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(): A2uiSurfaceStoreReturns: 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;
/** Live-stream entry point: applies a batch of envelopes for a tool call. */
applyPartialArgs(toolCallId: string, envelopes: readonly A2uiMessage[]): void;
/** True if a tool_call_id has produced live envelopes via applyPartialArgs. */
isPartialLive(toolCallId: string): boolean;
/** 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>;
/** Chat-side projections with per-component readiness. */
readonly surfaceStates: Signal<Map<string, A2uiSurfaceState>>;
surfaceState(surfaceId: string): Signal<A2uiSurfaceState | undefined>;
}apply(message)
Processes one A2uiMessage and updates the internal surfaces signal. All four message types are handled:
| Message type | Behavior |
|---|---|
createSurface | Records the surface's catalog, theme, and sendDataModel flag. For an already-live surface, tolerated as an idempotent refresh of those create-time fields |
updateComponents | Merges the provided components into the surface's component map by id — existing components are replaced, others are kept |
updateDataModel | Applies the envelope's value at its JSON-pointer path (see below) |
deleteSurface | Removes the surface (and any buffered pre-commit state) entirely |
The commit rule. A surface becomes visible once its createSurface envelope has arrived and a component with id root has been defined — the v0.9 progressive-rendering condition. Everything received earlier (components, data-model deltas) is buffered and committed together at that point. Afterwards, components merge incrementally by id and data-model updates apply immediately.
applyPartialArgs(toolCallId, envelopes)
The live-streaming entry point. Iterates envelopes and feeds each through apply(), recording the tool_call_id so the wrapped-content classifier can short-circuit duplicate dispatch when the final AI message arrives. Use isPartialLive(toolCallId) to check whether a tool call already streamed envelopes live.
surfaces
A readonly Signal<Map<string, A2uiSurface>> containing all committed 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 the commit rule is met, and undefined again after deleteSurface removes it.
const dashboard = store.surface('dashboard');
// dashboard() is A2uiSurface | undefinedsurfaceStates / surfaceState(surfaceId)
The chat-side projection consumed by the progressive renderer. Each A2uiSurfaceState pairs the wire-format surface with a componentViews map that tracks per-component readiness: a component's view flips ready once every data-model path it references has resolved, and readiness is monotonic — once ready, a later update clearing a referenced path does not revert it to a fallback.
updateDataModel Semantics
The updateDataModel envelope carries an optional JSON-pointer path and an optional plain-JSON value:
valuepresent — the data atpathis replaced (or created). Whenpathis omitted or'/', the whole model is replaced byvalue.valueomitted — the key atpathis deleted. With nopath, the model resets to{}. Deleting an array index sets it toundefinedand preserves the array's length (the v0.9 rule).
// Replace the whole model
{
"version": "v0.9",
"updateDataModel": {
"surfaceId": "s1",
"value": { "name": "Alice", "score": 42 }
}
}
// Set a nested value
{
"version": "v0.9",
"updateDataModel": {
"surfaceId": "s1",
"path": "/profile/approved",
"value": true
}
}
// Delete a key (no value field)
{
"version": "v0.9",
"updateDataModel": {
"surfaceId": "s1",
"path": "/profile/draft"
}
}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);
}
});The parser expects each line to carry a version plus one envelope key: {"version":"v0.9","createSurface":{...}}, {"version":"v0.9","updateComponents":{...}}, 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;
sendDataModel?: boolean;
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. catalogId, theme, and sendDataModel come from the surface's createSurface envelope.