showcase/shell-docs/src/content/reference/angular/functions/registerComponent.mdx
registerComponent registers a standalone Angular component as a tool the agent can call to display it. When the agent calls the tool, CopilotKit renders your component in the chat with the tool call's arguments. There is no handler, no user interaction, and no server-side execution: the agent decides when to show the component and populates its data.
This is the simplest form of generative UI, and the only one that needs nothing on the agent side. The tool is declared by the frontend and forwarded to the agent over AG-UI, so it behaves the same behind a Python agent as a TypeScript one. Contrast registerRenderToolCall, which draws a tool the agent already has and therefore requires that tool to exist in the agent.
You call registerComponent inside an Angular injection context (a component or service constructor, or a field initializer). The registration happens immediately and is removed when the owning injector is destroyed.
registerComponent is the Angular counterpart of useComponent in @copilotkit/react-core/v2 and @copilotkit/vue/v2. It builds the same model-facing tool description, so the tool reads identically to the model whichever frontend registered the component.
import { registerComponent } from "@copilotkit/angular";
function registerComponent<Args extends Record<string, unknown>>(
config: RegisterComponentConfig<Args>,
): void;
There is no handler field. A display-only component runs no application code, and CopilotKit completes the agent's turn with an empty tool result rather than an invented one. If you want to run browser code as well as render, use registerFrontendTool with both handler and component.
registerComponent returns void. It registers the tool and its renderer as a side effect, and removes both when the owning injector is destroyed.
Your component implements the ToolRenderer<Args> interface — the same contract every other Angular renderer uses. Declare the toolCall input with Angular's input.required() and read toolCall().args:
import { Signal } from "@angular/core";
interface ToolRenderer<Args extends Record<string, unknown>> {
toolCall: Signal<AngularToolCall<Args>>;
}
toolCall() is a discriminated union keyed on status. While the model is still streaming the payload the status is in-progress and args is Partial<Args>, so narrow on status before reading a field you require. See registerRenderToolCall for the full union.
import { Component, input } from "@angular/core";
import { AngularToolCall, ToolRenderer } from "@copilotkit/angular";
type IncidentArgs = { id: string; severity: string; summary: string };
@Component({
selector: "app-incident-card",
standalone: true,
template: `
@let call = toolCall();
@if (call.status === "in-progress") {
<div class="text-sm opacity-70">Loading incident…</div>
} @else {
<article class="rounded-lg border p-4">
<header class="flex items-baseline justify-between">
<strong>{{ call.args.id }}</strong>
<span class="text-xs uppercase">{{ call.args.severity }}</span>
</header>
<p class="text-sm">{{ call.args.summary }}</p>
</article>
}
`,
})
export class IncidentCardComponent implements ToolRenderer<IncidentArgs> {
readonly toolCall = input.required<AngularToolCall<IncidentArgs>>();
}
Register it once, anywhere under provideCopilotKit:
import { Component } from "@angular/core";
import { z } from "zod";
import { registerComponent } from "@copilotkit/angular";
import { IncidentCardComponent } from "./incident-card.component";
@Component({
selector: "app-chat",
standalone: true,
template: ``,
})
export class ChatComponent {
constructor() {
registerComponent({
name: "show_incident",
description: "Show one incident from the incident table.",
parameters: z.object({
id: z.string().describe("The incident id, such as INC-4711"),
severity: z.string().describe("One of sev1, sev2, sev3"),
summary: z.string().describe("One sentence on what happened"),
}),
component: IncidentCardComponent,
});
}
}
Nothing is added to the agent. The tool reaches it in the run's tool list, and the agent calls it by name.
registerComponent({
name: "show_incident",
parameters: incidentSchema,
component: IncidentCardComponent,
agentId: "support-agent",
});
A component that renders correctly over records your application does not hold looks identical to a correct one, in the browser and in a screenshot alike. The model fills these props from what it knows, so a component whose data never reached the agent is drawn from what the agent invented.
Registering the component is the rendering half. Give the agent the data it should describe with CopilotKitAgentContext or connectAgentContext, then check the rendered fields against the records your application holds.