Files
Benjamin Taylor 0b4f6ca392 fix(threads-drawer): clear stale fetch-more error, trap Escape while confirming, shield rows behind open menu (ENT-1051)
Addresses PR #5823 review (MikeRyanDev + marthakelly):

- core/threads.ts: a full-list refetch (filter-change / retry) now clears
  fetchMoreError on both listRequested and listSucceeded, so the inline
  'couldn't load more - retry' banner no longer survives onto a fresh list.
- web-components: Escape while the confirm-delete <dialog> is open is now
  consumed by a confirm guard in the host keydown handler; previously the
  bubbled keydown fell through to the mobile branch and closed the whole drawer
  along with the confirmation.
- web-components: an open kebab popover now shields the rest of the list -
  .list.menu-open .row:not(.menu-open) gets pointer-events:none so other rows
  no longer reveal their kebab or paint a host ::part(row):hover background
  around/behind the menu. Click-away dismissal is preserved via the existing
  document pointerdown handler.
- README: drop the removed --cpk-drawer-rail-width from the documented tokens.

Tests: core clears-fetchMoreError-on-refetch; web-components Escape-while-
confirming, confirm-dialog backdrop-click, menu-open row shield; Angular
scoped-chat-input focus (ancestor copilot-chat-view over the global fallback).
Menu-shield verified live in the langgraph-js example (:3002).
2026-07-07 14:01:12 -05:00
..
2026-04-10 23:38:59 +00:00

CopilotKit for Angular

Angular bindings for CopilotKit core and AG-UI agents. This package provides services, directives, and utilities for building custom, headless Copilot UIs.

Installation

# npm
npm install @copilotkit/angular

Peer dependencies you provide in your app:

  • @angular/core and @angular/common (19, 20, or 21)
  • @angular/cdk (match your Angular major)
  • rxjs

Quick start

1) Provide CopilotKit

Configure runtime and tools in your app config:

import { ApplicationConfig } from "@angular/core";
import { provideCopilotKit } from "@copilotkit/angular";

export const appConfig: ApplicationConfig = {
  providers: [
    provideCopilotKit({
      runtimeUrl: "http://localhost:3001/api/copilotkit",
      headers: { Authorization: "Bearer ..." },
      properties: { app: "demo" },
    }),
  ],
};

2) Build a custom UI with injectAgentStore

import { Component, inject, signal } from "@angular/core";
import { Message } from "@ag-ui/client";
import { CopilotKit, injectAgentStore } from "@copilotkit/angular";
import { randomUUID } from "@copilotkit/shared";

@Component({
  template: `
    @for (let message of messages(); track message.id) {
      <div>
        <em>{{ message.role }}</em>
        <p>{{ message.content }}</p>
      </div>
    }

    <input
      [value]="input()"
      (input)="input.set($any($event.target).value)"
      (keyup.enter)="send()"
    />
    <button (click)="send()" [disabled]="store().isRunning()">Send</button>
  `,
})
export class HeadlessChatComponent {
  readonly copilotKit = inject(CopilotKit);
  readonly store = injectAgentStore("default");
  readonly messages = this.store().messages;

  readonly input = signal("");

  async send() {
    const content = this.input().trim();
    if (!content) return;

    const agent = this.store().agent;

    agent.addMessage({
      id: randomUUID(),
      role: "user",
      content,
    });

    this.input.set("");

    await this.copilotKit.core.runAgent({ agent });
  }
}

The agent is an AG-UI AbstractAgent. Refer to your AG-UI agent implementation for available methods and message formats.

Core configuration

CopilotKitConfig

provideCopilotKit accepts a CopilotKitConfig object:

export interface CopilotKitConfig {
  runtimeUrl?: string;
  headers?: Record<string, string>;
  licenseKey?: string;
  properties?: Record<string, unknown>;
  agents?: Record<string, AbstractAgent>;
  tools?: ClientTool[];
  renderToolCalls?: RenderToolCallConfig[];
  frontendTools?: FrontendToolConfig[];
  humanInTheLoop?: HumanInTheLoopConfig[];
}
  • runtimeUrl: URL to your CopilotKit runtime.
  • headers: Default headers sent to the runtime.
  • properties: Arbitrary props forwarded to agent runs.
  • agents: Local, in-browser agents keyed by agentId.
  • tools: Tool definitions advertised to the runtime (no handler).
  • renderToolCalls: Components to render tool calls in the UI.
  • frontendTools: Client-side tools with handlers.
  • humanInTheLoop: Tools that pause for user input.

Injection helpers

  • provideCopilotKit(config): Provider for CopilotKitConfig.

CopilotKit service

Readonly signals

  • agents: Signal<Record<string, AbstractAgent>>
  • runtimeConnectionStatus: Signal<CopilotKitCoreRuntimeConnectionStatus>
  • runtimeUrl: Signal<string | undefined>
  • runtimeTransport: Signal<CopilotRuntimeTransport> ("rest" | "single")
  • headers: Signal<Record<string, string>>
  • toolCallRenderConfigs: Signal<RenderToolCallConfig[]>
  • clientToolCallRenderConfigs: Signal<FrontendToolConfig[]>
  • humanInTheLoopToolRenderConfigs: Signal<HumanInTheLoopConfig[]>

Methods

  • getAgent(agentId: string): AbstractAgent | undefined
  • addFrontendTool(config: FrontendToolConfig & { injector: Injector }): void
  • addRenderToolCall(config: RenderToolCallConfig): void
  • addHumanInTheLoop(config: HumanInTheLoopConfig): void
  • removeTool(toolName: string, agentId?: string): void
  • updateRuntime(options: { runtimeUrl?: string; runtimeTransport?: CopilotRuntimeTransport; headers?: Record<string,string>; properties?: Record<string, unknown>; agents?: Record<string, AbstractAgent>; }): void

Advanced

  • core: The underlying CopilotKitCore instance.

Agents

injectAgentStore

const store = injectAgentStore("default");
// or: injectAgentStore(signal(agentId))

Returns a Signal<AgentStore>. The store exposes:

  • agent: AbstractAgent
  • messages: Signal<Message[]>
  • state: Signal<any>
  • isRunning: Signal<boolean>
  • teardown(): Clean up subscriptions

If the agent is not available locally but a runtimeUrl is configured, a proxy agent is created while the runtime connects. If the agent still cannot be resolved, an error is thrown that includes the configured runtime and known agent IDs.

CopilotkitAgentFactory

Advanced factory for creating AgentStore signals. Most apps should use injectAgentStore instead.

Agent context

connectAgentContext

Connect AG-UI context to the runtime (auto-cleanup when the effect is destroyed):

import { connectAgentContext } from "@copilotkit/angular";

connectAgentContext({
  description: "User preferences",
  value: { theme: "dark" },
});

You must call it within an injection context (e.g., inside a component constructor or runInInjectionContext), or pass an explicit Injector:

connectAgentContext(contextSignal, { injector });

Tools and tool rendering

Types

export interface RenderToolCallConfig<Args> {
  name: string;              // tool name, or "*" for wildcard
  args: z.ZodType<Args>;      // Zod schema for args
  component: Type<ToolRenderer<Args>>;
  agentId?: string;           // optional agent scope
}

export interface FrontendToolConfig<Args> {
  name: string;
  description: string;
  parameters: z.ZodType<Args>;
  component?: Type<ToolRenderer<Args>>; // optional UI renderer
  handler: (args: Args, context: FrontendToolHandlerContext) => Promise<unknown>;
  agentId?: string;
}

export interface HumanInTheLoopConfig<Args> {
  name: string;
  description: string;
  parameters: z.ZodType<Args>;
  component: Type<HumanInTheLoopToolRenderer<Args>>;
  agentId?: string;
}

export type ClientTool<Args> = Omit<FrontendTool<Args>, \"handler\"> & {
  renderer?: Type<ToolRenderer<Args>>;
};

Renderer components receive a signal:

export interface ToolRenderer<Args> {
  toolCall: Signal<AngularToolCall<Args>>;
}

export interface HumanInTheLoopToolRenderer<Args> {
  toolCall: Signal<HumanInTheLoopToolCall<Args>>; // includes respond(result)
}

AngularToolCall / HumanInTheLoopToolCall expose args, status ("in-progress" | "executing" | "complete"), and result.

Register tools with DI

These helpers auto-remove tools when the current injection context is destroyed: Call them from an injection context (e.g., a component constructor, directive, or runInInjectionContext).

import {
  registerFrontendTool,
  registerRenderToolCall,
  registerHumanInTheLoop,
} from "@copilotkit/angular";
import { z } from "zod";

registerFrontendTool({
  name: "lookup",
  description: "Fetch a record",
  parameters: z.object({ id: z.string() }),
  handler: async ({ id }) => ({ id, ok: true }),
});

registerRenderToolCall({
  name: "*", // wildcard renderer
  args: z.any(),
  component: MyToolCallRenderer,
});

registerHumanInTheLoop({
  name: "approval",
  description: "Request approval",
  parameters: z.object({ reason: z.string() }),
  component: ApprovalRenderer,
});

Configure tools in provideCopilotKit

provideCopilotKit({
  frontendTools: [
    /* FrontendToolConfig[] */
  ],
  renderToolCalls: [
    /* RenderToolCallConfig[] */
  ],
  humanInTheLoop: [
    /* HumanInTheLoopConfig[] */
  ],
  tools: [
    /* ClientTool[] */
  ],
});

tools are advertised to the runtime. If you include renderer + parameters on a ClientTool, CopilotKit will also register a renderer for tool calls.

RenderToolCalls component

RenderToolCalls renders tool call components under an assistant message based on registered render configs.

<copilot-render-tool-calls
  [message]="assistantMessage"
  [messages]="messages"
  [isLoading]="isRunning"
></copilot-render-tool-calls>

Inputs:

  • message: AssistantMessage (must include toolCalls)
  • messages: full Message[] list (used to find tool results)
  • isLoading: whether the agent is currently running

Tool arguments are parsed with partialJSONParse, so incomplete JSON during streaming still renders.

Runtime notes

  • Set runtimeUrl to your CopilotKit runtime endpoint.
  • If you need to change runtime settings at runtime, call CopilotKit.updateRuntime(...).
  • runtimeTransport supports "rest" or "single" (SSE single-stream transport).

Not documented here

This package also exports a full set of chat UI components under src/lib/components/chat. Those APIs are intentionally omitted from this README.