Render · API Reference

#title: views() description: Create and compose view registries for generative UI rendering

views()

Creates an immutable view registry mapping names to Angular components. Views are rendered inline in the chat when the agent produces a JSON spec.

#Usage

import { views } from '@threadplane/render';
 
const ui = views({
  'plan-checklist': PlanChecklistComponent,
  'file-preview': FilePreviewComponent,
  'code-output': CodeOutputComponent,
});

Pass to the chat component:

<chat [agent]="stream" [views]="ui" />

Or provide globally via DI:

import { provideViews } from '@threadplane/render';
 
// app.config.ts
providers: [provideViews(ui)]

#Composition

Compose registries via object spread. Last key wins for overrides:

const all = views({
  ...thirdPartyViews,
  ...myViews,
  'chart': MyCustomChart, // overrides thirdPartyViews['chart']
});

#Per-View Fallbacks

Each entry can be a bare component or a { component, fallback } object. The fallback mounts while a state-bound prop on the view is still unresolved -- the same shape defineAngularRegistry() accepts:

const ui = views({
  'plan-checklist': {
    component: PlanChecklistComponent,
    fallback: PlanSkeletonComponent,
  },
  'file-preview': FilePreviewComponent, // bare form uses the default fallback
});

withViews() and overrideViews() accept the same object form in their addition and override maps.

#API

#views(map)

Creates a frozen ViewRegistry from a Record<string, Type<unknown>>.

ParameterTypeDescription
mapRecord<string, Type<unknown>>Name → Angular component mapping
ReturnsViewRegistryFrozen immutable registry

#withViews(base, additions)

Adds new views without overwriting existing entries. Existing keys in base are preserved.

const extended = withViews(base, {
  'new-widget': NewWidget,    // added
  'existing': Other,          // IGNORED — base already has 'existing'
});

#overrideViews(base, overrides)

Replaces entries in a registry. Keys in overrides win over base. Use this when you want to swap an existing renderer; use withViews when you want to add new node types without touching existing ones.

import { overrideViews } from '@threadplane/render';
 
const myRegistry = overrideViews(baseRegistry, {
  'code-block': MyCodeBlockComponent,
});

Returns a new frozen ViewRegistry. The base argument is not mutated.

#withoutViews(base, ...names)

Removes views by name:

const restricted = withoutViews(base, 'dangerous-widget', 'internal-tool');

#provideViews(registry)

Angular DI provider. Works at app level or route level:

// Global
providers: [provideViews(ui)]
 
// Route-scoped
{ path: 'planning', providers: [provideViews(planningViews)] }

<render-spec> and <render-element> consume VIEW_REGISTRY as a third-priority fallback in registry resolution. The full priority order is: the [registry] template input → RENDER_CONFIG.registry (from provideRender(...)) → VIEW_REGISTRY (from provideViews(...)) → the existing empty fallback. So provideViews(myRegistry) drives rendering when no provideRender({ registry }) is wired and no [registry] input is bound.

#toRenderRegistry(registry)

Converts a ViewRegistry to the low-level AngularRegistry type used by <render-spec>. Called internally by the chat component — most developers won't need this.

#View Components

View components are standard Angular standalone components. They receive props as input() signals:

@Component({
  selector: 'plan-checklist',
  standalone: true,
  template: `
    <div class="border rounded-xl p-4">
      <h4>{{ title() }}</h4>
      <ng-content />
    </div>
  `,
})
export class PlanChecklistComponent {
  readonly title = input<string>('Plan');
}

#How Specs Are Detected

The chat component checks each message for a ui field containing a valid spec:

{
  "type": "tool",
  "content": "...",
  "ui": {
    "root": "plan",
    "elements": {
      "plan": {
        "type": "plan-checklist",
        "props": { "title": "My Plan" }
      }
    }
  }
}

The type in the spec is matched against the view registry to resolve the Angular component.