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>>.
| Parameter | Type | Description |
|---|---|---|
map | Record<string, Type<unknown>> | Name → Angular component mapping |
| Returns | ViewRegistry | Frozen 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.
Related
- defineAngularRegistry() -- the low-level
AngularRegistrythattoRenderRegistry()converts aViewRegistryinto - provideRender() -- global render config, including a registry
- Chat generative-UI guide -- wiring
views()into chat so an agent can stream specs that render inline