feat(copilotkit): dockable CopilotKit chat sidebar on AG-UI - #122
feat(copilotkit): dockable CopilotKit chat sidebar on AG-UI#122jerelvelarde wants to merge 9 commits into
Conversation
|
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. |
There was a problem hiding this comment.
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; |
|
|
||
| override run(input: RunAgentInput): Observable<BaseEvent> { | ||
| return new Observable<BaseEvent>(subscriber => { | ||
| this.cancelled = false; |
There was a problem hiding this comment.
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).
| this.cancelled = false; | |
| let cancelled = false; |
| // Wire up the A2UI generation/repair tool-call renders exactly once, from | ||
| // within this component's injection context (required by CopilotKit DI). | ||
| registerA2uiToolRenders(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| constructor() { | ||
| super({agentId: 'default'}); | ||
| } |
There was a problem hiding this comment.
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.
| 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()); |
There was a problem hiding this comment.
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.
| readonly hasKey = computed(() => !!this.configProvider.geminiApiKey().trim()); | |
| readonly hasKey = computed(() => !!this.configProvider.geminiApiKey()); |
References
- 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.
0982913 to
21856ca
Compare
|
Addressed the Gemini review:
Rebased onto latest |
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.
21856ca to
ee2c59f
Compare
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
GeminiA2uiAgent(wraps the existing client-side Gemini flow — no backend/runtime), emitting A2UI via arender_a2uitool call.render_a2ui+a2ui_repairtool-renders;A2uiGenerationServiceextracted fromChatCoordinator.CopilotSidebarreplacing the Gemini panel; A2UI JSON editor hidden at first load with a Show/Hide-source toggle + first-generation auto-reveal.@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 buildOK on the source branch.Notes
@copilotkit/angular@0.1.2peer-caps at Angular^19||^20||^21; it runs on the composer's Angular 22 as an owned "unsupported combo" (tsc clean,fesm2022links, renders at runtime). Needs a rootresolutions: { "rxjs": "7.8.2" }to dedupe rxjs across@ag-ui/client/@copilotkit/core.Contributed by CopilotKit.
Screenshots
The docked CopilotKit sidebar ("Assistant") with the bring-your-own-key gate, replacing the in-panel Gemini card: