Skip to content

feat(copilotkit): dockable CopilotKit chat sidebar on AG-UI - #122

Open
jerelvelarde wants to merge 9 commits into
a2ui-project:mainfrom
jerelvelarde:jerel/pr4-copilotkit-sidebar
Open

feat(copilotkit): dockable CopilotKit chat sidebar on AG-UI#122
jerelvelarde wants to merge 9 commits into
a2ui-project:mainfrom
jerelvelarde:jerel/pr4-copilotkit-sidebar

Conversation

@jerelvelarde

@jerelvelarde jerelvelarde commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Replaces the in-panel Gemini Assistant card with a docked CopilotKit v2 chat sidebar on AG-UI — same prompt → A2UI-in-preview loop, with custom tools and a friendlier surface.

What

  • In-browser GeminiA2uiAgent (wraps the existing client-side Gemini flow — no backend/runtime), emitting A2UI via a render_a2ui tool call.
  • render_a2ui + a2ui_repair tool-renders; A2uiGenerationService extracted from ChatCoordinator.
  • Docked, collapsible CopilotSidebar replacing the Gemini panel; A2UI JSON editor hidden at first load with a Show/Hide-source toggle + first-generation auto-reveal.
  • Bring-your-own-Gemini-key gate, starter suggestion chips, and streamed model reasoning shown inline.
  • Adds @copilotkit/angular.

Why

A friendlier, extensible chat UX with custom tools / tool-renders and native A2UI tooling, in place of the raw Gemini card.

Testing

Full shell test suite green, ng build OK on the source branch.

Notes

  • Biggest architectural change of the four (removes the chat panel, adds the sidebar).
  • Heads-up: @copilotkit/angular@0.1.2 peer-caps at Angular ^19||^20||^21; it runs on the composer's Angular 22 as an owned "unsupported combo" (tsc clean, fesm2022 links, renders at runtime). Needs a root resolutions: { "rxjs": "7.8.2" } to dedupe rxjs across @ag-ui/client / @copilotkit/core.
  • Happy to align on shape + the runtime story before this one moves.

Contributed by CopilotKit.

Screenshots

The docked CopilotKit sidebar ("Assistant") with the bring-your-own-key gate, replacing the in-panel Gemini card:

Light Dark

@google-cla

google-cla Bot commented Jul 22, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the native Gemini Assistant card with a collapsible CopilotKit v2 sidebar, extracting the core generation pipeline into a headless A2uiGenerationService and introducing an in-browser GeminiA2uiAgent with custom tool-renders. Feedback on the changes highlights a critical concurrency risk in the singleton agent due to a class-level cancelled property, recommending scoping it locally to the observable subscription. Additionally, reviewers suggest moving the tool-call render registration from the non-singleton sidebar constructor to the singleton agent's constructor to prevent duplicate registrations, and advise trimming configuration strings at the source rather than at usage sites.

@Injectable({providedIn: 'root'})
export class GeminiA2uiAgent extends AbstractAgent {
private readonly generation = inject(A2uiGenerationService);
private cancelled = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Remove the class-level cancelled property to prevent shared state bugs across concurrent runs of this singleton agent.


override run(input: RunAgentInput): Observable<BaseEvent> {
return new Observable<BaseEvent>(subscriber => {
this.cancelled = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Declare cancelled as a local variable scoped to the individual Observable subscription. This ensures that concurrent agent runs do not interfere with each other's cancellation states.

Note: Remember to also update all references from this.cancelled to cancelled within the run method (lines 57, 75, 92, 120, 129).

Suggested change
this.cancelled = false;
let cancelled = false;

Comment on lines +95 to +97
// Wire up the A2UI generation/repair tool-call renders exactly once, from
// within this component's injection context (required by CopilotKit DI).
registerA2uiToolRenders();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling registerA2uiToolRenders() inside the constructor of CopilotSidebar (which is a non-singleton component) will cause the tools to be registered multiple times whenever the sidebar is destroyed and recreated (e.g., when navigating away and back). This can lead to duplicate registration errors or memory leaks.

Since registerA2uiToolRenders() requires an injection context, a safer and cleaner architectural choice is to call it inside the constructor of a singleton service, such as GeminiA2uiAgent (which is @Injectable({providedIn: 'root'})). This ensures the tools are registered exactly once for the lifetime of the application.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I looked into this and it isn't safe to move as-is. Registering the A2UI renders in GeminiA2uiAgent's constructor creates an NG0200 circular dependency: registerRenderToolCall() calls inject(CopilotKit), but GeminiA2uiAgent is instantiated by the COPILOT_KIT_CONFIG factory during CopilotKit's own construction, so the cycle is CopilotKit → COPILOT_KIT_CONFIG → GeminiA2uiAgent → CopilotKit (repro: NG0200: Circular dependency detected for CopilotKit).

I've kept registration in the sidebar's injection context, where destroyRef.onDestroy cleans it up so re-created panels don't leak. If you'd prefer it out of the component, two cycle-free options: (a) a provideAppInitializer in app.config.ts that runs after CopilotKit exists, or (b) a lazy, once-guarded registration inside the agent on first run() via a captured Injector + runInInjectionContext. Happy to do whichever you prefer.

Comment on lines +45 to +47
constructor() {
super({agentId: 'default'});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Register the A2UI tool-call renders here in the singleton agent's constructor. This ensures they are registered exactly once during the application lifecycle and avoids duplicate registration issues when the sidebar component is destroyed and recreated.

Note: Remember to import registerA2uiToolRenders from ../register-tool-renders in this file.

Suggested change
constructor() {
super({agentId: 'default'});
}
constructor() {
super({agentId: 'default'});
registerA2uiToolRenders();
}

* bring-your-own-key and runs client-side, so the chat must not mount until a
* non-blank key exists.
*/
readonly hasKey = computed(() => !!this.configProvider.geminiApiKey().trim());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the general rules, configuration strings (such as API keys) must be trimmed at the source (during initialization or storage) rather than at every individual check or usage site. This maintains robust validation and prevents whitespace-only values from causing runtime failures.

Please remove the .trim() call here and ensure the key is trimmed at the source.

Suggested change
readonly hasKey = computed(() => !!this.configProvider.geminiApiKey().trim());
readonly hasKey = computed(() => !!this.configProvider.geminiApiKey());
References
  1. Ensure configuration strings (such as API keys) are trimmed at the source (during initialization or storage) rather than at every individual check or usage site to maintain robust validation and prevent whitespace-only values from causing runtime failures.

@jerelvelarde
jerelvelarde marked this pull request as ready for review July 23, 2026 12:23
@jerelvelarde
jerelvelarde force-pushed the jerel/pr4-copilotkit-sidebar branch 3 times, most recently from 0982913 to 21856ca Compare July 29, 2026 17:19
@jerelvelarde

Copy link
Copy Markdown
Contributor Author

Addressed the Gemini review:

  • (critical) gemini-a2ui-agent.ts — removed the class-level cancelled field; it's now a per-run local inside run(), so concurrent runs on the root singleton no longer share cancellation state.
  • (medium) copilot-sidebar.tshasKey no longer re-trims; AppConfigProvider already trims at the source (verified in settings.ts / local-storage-config.provider.ts).
  • ⚠️ (high) "register renders in the agent constructor" — not applied: it creates an NG0200 circular dependency (CopilotKit → COPILOT_KIT_CONFIG → GeminiA2uiAgent → CopilotKit). Full rationale and two cycle-free alternatives are in the inline reply on that thread.

Rebased onto latest main; full shell suite (756 tests) green.

In-browser AG-UI agent wrapping the existing client-side Gemini flow;
render_a2ui tool render instead of raw JSONL; docked CopilotKit sidebar;
A2UI JSON editor hidden at first load. No backend.
- add suggestions + repair tool-renders to v1 (agreed scope)
- clarify narration is agent-synthesized (raw JSONL never hits the thread),
  keeping generation functionally identical
- return heals[] from A2uiGenerationService for the repair render
- add spikes/risks: RenderA2UIArgsSchema compat, single injectAgentStore
  per agentId, live catalog read (no mid-run reconfigure)
- record option-1/Angular-21 track as dropped; converged on Angular 22
Move the A2UI generation pipeline (system prompt, streamed accumulation,
self-healing JSONL parser, catalog component schema healing, and gateway
error mapping) out of ChatCoordinator into a new injectable
A2uiGenerationService so a CopilotKit AG-UI agent can drive generation
without chat-bubble side effects.

- A2uiGenerationService.generate() runs the full pipeline and returns a
  GenerationResult discriminated union (never throws for expected
  generation/validation/connectivity failures); on success it commits via
  StateSync.commitLayoutFromLlm and reports component-name heals.
- runCatalogComponentSchemaCheck now returns the applied heals.
- ChatCoordinator keeps its chat-bubble UX and TEST_ONLY hook, delegating
  the moved pipeline methods and systemPrompt to the service.
…load, remove probe

Replace the Gemini chat Dockview panel with a docked, collapsible CopilotKit
sidebar mounted as a flex sibling of the Dockview root, and hide the A2UI JSON
editor ("Raw") at first load. A "Show source" toolbar button reveals/hides the
editor on demand, and it auto-reveals once the first generation reaches READY
(unless the user already toggled it). Removes the _ck-probe spike.
Gate the docked assistant on a Gemini API key: the chat mounts only when a
non-blank key exists, otherwise a routerLink /settings CTA mirroring the
chat-panel is shown (bring-your-own-key, client-side).

Publish starter-prompt chips natively via a CopilotKit StaticSuggestionsConfig
(available: before-first-message, consumerAgentId: default) so <copilot-chat>
auto-renders them on an empty thread and routes a chosen chip's prompt through
the single shared default agent store. Starters are tailored to the active
catalog name when available, else a generic set. The config is registered only
while a key exists and withdrawn on key removal, starter change, and destroy.
…debar

A2uiGenerationService.generate() forwards Gemini thinking deltas via an
onThinking callback; GeminiA2uiAgent emits AG-UI REASONING_MESSAGE_* events so
CopilotKit renders a live collapsible reasoning bubble inline. Raw JSONL still
never reaches the thread.
The docs/superpowers spec is a CopilotKit-internal design draft, not intended for upstream.
Scope GeminiA2uiAgent cancellation to each run(): replace the class-level
`cancelled` field with a local declared inside the run() Observable, so
concurrent runs on this providedIn:'root' singleton no longer share
cancellation state.

Drop the redundant `.trim()` in CopilotSidebar's hasKey check.
AppConfigProvider already trims the Gemini API key at the source (covered
by local-storage-config.provider.spec.ts), so usages can trust the value.

Note: the suggestion to move registerA2uiToolRenders() into the singleton
GeminiA2uiAgent constructor is intentionally not applied. It introduces an
NG0200 circular dependency (CopilotKit -> COPILOT_KIT_CONFIG ->
GeminiA2uiAgent -> CopilotKit): the agent is constructed by the
COPILOT_KIT_CONFIG factory during CopilotKit's own construction, and
registerRenderToolCall injects CopilotKit. Registration therefore stays in
the sidebar component's injection context.
@jerelvelarde
jerelvelarde force-pushed the jerel/pr4-copilotkit-sidebar branch from 21856ca to ee2c59f Compare July 29, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant