Repository navigation
feat: pair Gemini + CopilotKit in the Composer assistant - #215
Conversation
There was a problem hiding this comment.
Code Review
This pull request integrates CopilotKit into the chat panel, replacing custom chat UI elements with CopilotKit components, and updates the shell's global styles and typography to use 'Plus Jakarta Sans' and 'Spline Sans Mono'. It also enhances the Components Gallery to support live editing of component properties and raw JSON drafts, enabling a seamless handoff to the Composer workspace. Feedback from the review highlights several critical opportunities to prevent runtime TypeError crashes, specifically by adding defensive nullish fallbacks for message content, safely accessing nested component arrays, handling unresolved property schemas, and robustly validating boolean or array types within JSON Schema matching.
|
Worked through all five automated review findings. Every one is already prevented on the current head — but four of the guards live in files the review did not have in view, and one was fixed in the commit immediately after the reviewed revision (
No production code changed. What the findings did expose is that three of these guards were enforced only by convention — a later refactor could have removed one without failing a test. Validation on |
⚡ A2UI Composer PR PreviewYour automated preview is successfully live (commit
|
a96252b to
0e6cbb4
Compare
|
Synced with main through #217, #119, and #129. Head is now Main changed the same chat-panel files this branch rewrites, so I carried its changes into the CopilotKit panel:
Two other fixes: the disabled assistant panel is back on |
|
Synced with the latest main (
Formatting, lint, typechecks, the production build, and all unit tests pass. Full e2e before that test fix: 79 passed, with 1 flaky (the React shrink test that is also flaky on unchanged main); the fixed spec now passes on its own. |
|
Synced with main (
Full e2e: everything passes apart from main's custom-instructions journey, which fails the same way on unchanged main. |
A catalog property typed as, e.g., ["string", "null"] now accepts any of the listed types. Moved here from a2ui-project#215 so the assistant PR no longer changes Gallery code.
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
9fee5c5 to
31bef1b
Compare
|
Rebuilt so this no longer depends on #129. The branch is now one commit on Full e2e: 78 pass, including all six assistant journeys. The one failure is main's custom-instructions journey, which fails the same way on unchanged main. |
| } | ||
|
|
||
| private resolveLayoutUpdate(updates: unknown[]): unknown[] { | ||
| const surfaceId = (item: unknown, command: string): string | undefined => { |
There was a problem hiding this comment.
Please add comments explaining what this is all doing and why it needs to be done.
There was a problem hiding this comment.
Added a doc comment in a2dfceb, plus inline comments on each step.
In short, the model can answer an edit two ways. A full replacement starts with createSurface and replaces the draft as-is. An incremental edit only sends updates to surfaces that already exist. Because the editor replays the whole message list to render the preview, an incremental edit written on its own would erase the draft, so it's appended to the current draft instead.
Before appending, every surface an update targets is checked against the surfaces the draft and the response create (minus the ones they delete). If the model invents a surface ID, for example by copying one from the prompt's examples, it's rejected like any other validation failure and the draft is left unchanged.
| import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; | ||
| import {FileIngestionService, AttachedFile} from '../file-ingestion/file-ingestion.service'; | ||
| import {FormsModule} from '@angular/forms'; | ||
| import { |
There was a problem hiding this comment.
Instead of these changes, I think we need to have separate chat panel implementations for the "plain chat panel" and the CopilotKit chat panel, and use dependency injection.
We could have a common base class that has all the common stuff.
There was a problem hiding this comment.
Done in a2dfceb.
ChatPanelBaseholds the shared state and actions: submitting and retrying prompts, attachments, screenshots, renderer switching, the instruction dialogs, and how history is presented.ChatPanelis the plain panel. Its template and styles are main's, unchanged, and it now extends the base.CopilotKitChatPanelextends the same base. It lives inchat/copilotkit-chat-panel/with its template, styles, and theme, soglobal_styles.scssis back to main's.- The workspace mounts whatever
CHAT_PANEL_COMPONENTprovides. It defaults toChatPanel, and the open-sourceapp.config.tsprovides the CopilotKit panel. That provider is the only reference to CopilotKit outside its folder, so a build that drops it never imports@copilotkit/angular.
Both panels after the same edit, light and dark (CopilotKit on the left of each pair, the plain panel on the right):
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
|
Why are we adding a renderer selector to the chat panel? This might be interesting, but it seems to be orthogonal to the point of this PR. I'd be interested in hearing more about this aspect of the change -- it might well be a nice additional, but I don't know that changing renderers is something users will do that often. But, I also recognize that as you look to bring more renderers on board (Slack, Teams, etc...), it actually might be very useful. I suggest we pull this out from this PR and create a new PR with just that feature so we can discuss it more. |
| } | ||
| } | ||
|
|
||
| function installFixtureInBrowser(fakeApiKey: string): void { |
There was a problem hiding this comment.
This is doing a bunch of surgery... Can we add comments to the code explaining what's going on and why?
There was a problem hiding this comment.
Added comments in e3259dc. The file header explains why it works this way: Composer calls Gemini from the browser through the @google/genai SDK, which streams server-sent events over fetch. page.route can only answer with a complete body, so it can't send a response chunk by chunk or hold one open until the app cancels it, which the Stop journey needs. So the fixture replaces window.fetch inside the page before the app loads: it records Gemini requests for assertions, answers them from scripted scenarios, and passes every other request through. Each part now has a comment.
| return headers; | ||
| }; | ||
|
|
||
| const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms)); |
There was a problem hiding this comment.
I've found that using timeouts often leads to flaky tests or, if you make the timeout high enough to never be flaky, it causes the tests to just take a lot longer to run.
Are there elements we can use and wait for them to be "ready", taking advantage of Playwright's support for this instead?
There was a problem hiding this comment.
Agreed. The delay is gone in e3259dc. The only journey that used it was the Stop test, and it already holds the stream open until the app cancels it, so it waits for the Stop button, clicks it, and checks the result. There are no timers left in the fixture or the assistant journeys.
| ]; | ||
| await page.evaluate( | ||
| json => { | ||
| const monaco = ( |
There was a problem hiding this comment.
This feels like we're cheating :-)
Shouldn't we be locating the DOM element, and using locator.fill to populate the text?
There was a problem hiding this comment.
Fair point. locator.fill can't reach Monaco here: in Chrome, Monaco takes input through a textbox element that uses the EditContext API rather than a <textarea>, and typed or inserted text goes through its auto-closing brackets, which rewrites the JSON. So in e3259dc the journeys open their draft the way a user opens a shared design link: the test encodes the JSON exactly as the Share button does (d1. + deflate-raw + base64url, see QueryParser.compressPayload) and navigates to #a2ui=…. Nothing writes through Monaco's API anymore. Reading the draft back still uses the model, like main's getMonacoContent helper.
|
|
||
| function replaceDraftText(draft: string, from: string, to: string): string { | ||
| const updated = draft.replaceAll(from, to); | ||
| expect(updated).not.toBe(draft); |
There was a problem hiding this comment.
This expect seems like we're just testing the Javascript runtime.
|
|
||
| function luminanceChannel(channel: number): number { | ||
| const normalized = channel / 255; | ||
| return normalized <= 0.03928 ? normalized / 12.92 : Math.pow((normalized + 0.055) / 1.055, 2.4); |
There was a problem hiding this comment.
Please add a comment explaining all of this and the magic numbers.
Same for the contrastRatio method below.
There was a problem hiding this comment.
Those were the WCAG relative-luminance and contrast-ratio formulas. They're gone in e3259dc, replaced by axe (see the next comment).
| readonly sharedA2uiError = this.startupConfigState.sharedA2uiError; | ||
|
|
||
| private readonly destroyRef = inject(DestroyRef); | ||
| private rendererSelectionRequest = 0; |
There was a problem hiding this comment.
Why do we need to count the number of requests to change the renderer?
There was a problem hiding this comment.
It detected that another renderer selection had started (or startup had re-resolved the renderer) while the origin confirmation dialog was open, so the older request wouldn't overwrite the newer choice when the user answered. In #244 the counter is gone, per your next comment.
| this.startupConfigState.setSelectedRendererId(rendererId); | ||
| async setSelectedRendererId(rendererId: string | null, signal?: AbortSignal): Promise<boolean> { | ||
| signal?.throwIfAborted(); | ||
| const request = ++this.rendererSelectionRequest; |
There was a problem hiding this comment.
Instead of counting requests, can we just check if the desired renderer is not the active renderer (below on line 93)?
There was a problem hiding this comment.
Yes. In #244 it compares the selection before and after the confirmation instead: if the selected renderer, the resolved URL, or that renderer's configured URL changed while the dialog was open, the request is stale and is rejected. There's a comment explaining it.
| const rendererUrl = active?.rendererUrl; | ||
| if (rendererUrl) { | ||
| const isAllowed = signal | ||
| ? await this.isOriginAllowed(rendererUrl, signal) |
There was a problem hiding this comment.
Since the signal parameter to the isOriginAllowed method is optional, there's no need to use the elvis operator.
|
|
||
| async isOriginAllowed(url: string): Promise<boolean> { | ||
| private parseRendererUrl(url: string): URL { | ||
| const baseUrl = globalThis.document?.baseURI || this.environmentContext.getBaseOrigin(); |
There was a problem hiding this comment.
Why the added check against globalThis.document?.baseURI?
| const confirmed = await this.confirmOrigin(origin); | ||
| const confirmed = signal | ||
| ? await this.confirmOrigin(origin, signal) | ||
| : await this.confirmOrigin(origin); |
There was a problem hiding this comment.
Again, the signal parameter is optional, so this can be simplified.
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
Thanks for the thorough review. The renderer selector is now its own PR, #244. The reason for it is the one you mention: with the Slack renderer, and more formats to come, switching output formats from the prompt instead of Settings is what makes that workflow practical. #216 now builds on #244, since its assistant tool switches to the Slack renderer when you ask for a Slack message. This PR is now the CopilotKit panel behind DI plus the general fixes. The shared chat panel base is main's |
d2c581f to
ba97421
Compare
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
Gemini keeps generating and editing A2UI; CopilotKit's Angular chat interface (@copilotkit/angular 0.5.2) provides the Assistant panel's message cards and prompt composer. Composer's existing services still own model requests, catalog instructions, history, validation, and editor updates. - One rounded input pill with an Add (+) menu for attachments, a screenshot, Instructions, and Custom Instructions, plus a renderer selector that waits for the new renderer's catalog. - The current canvas is synchronized before every prompt, and the model is told to use the draft's real surface IDs, component IDs, and bindings. Incremental edits keep the full draft; updates to unknown surfaces are rejected and the last valid canvas is kept. - The conversation input survives canvas snapshot changes, and a restored draft is resynchronized after model changes. - Attachments, screenshots, retry, stop, copy, the parse-error card, and light/dark themes are preserved. This branch is built directly on main and no longer depends on the Gallery redesign in a2ui-project#129: the assistant journeys open their draft through the JSON editor instead of the Gallery.
The chat panel's shared state and actions move into ChatPanelBase. The dependency-free ChatPanel (main's template) and CopilotKitChatPanel both extend it, and the workspace mounts whichever CHAT_PANEL_COMPONENT names. The token defaults to ChatPanel; the open-source app provides the CopilotKit panel in app.config.ts. All CopilotKit code, including its stylesheet and theme, now lives in chat/copilotkit-chat-panel, so a build without that provider never imports @copilotkit/angular. Also documents resolveLayoutUpdate in ChatCoordinator.
…d name The selector no longer maps specific renderer IDs to its own labels, so new or internal renderers need no code change to appear correctly.
…nstructions journey The CopilotKit panel moves both instruction links into the Add (+) menu. The journey also waits for the dialog's open animation before measuring it.
…se as main's panel - Remove the renderer selector (the pill menu, RendererSelection, and the renderer switching changes in StartupResolution, SettingsService, and RenderedFrame). It moves to its own PR so it can be discussed separately. - ChatPanelBase is main's ChatPanel logic again: its history, status text, and bubble classes are unchanged. The only differences are that the class is abstract, two fields are protected, and submitPrompt repeats the Send button's checks because pressing Enter calls it directly. - The CopilotKit panel prepares its own view of the history, with comments on how and why it differs from the plain panel. - Add FORCE_PLAIN_CHAT_PANEL, a local storage flag that makes the app mount the plain panel, so browser tests can cover both panels.
- Run the custom instructions and malformed-stream journeys against both the CopilotKit panel and the plain panel. - Open the assistant journeys' draft through a shared design link instead of setting Monaco's model directly. - Check the chat panel with @axe-core/playwright instead of a hand-written contrast calculation. - Replace the dark-mode gradient check with a visual test of the CopilotKit panel. - Document the Gemini fixture and drop its timed delay; the Stop journey already holds the stream open until the app cancels it.
ba97421 to
3b50ad1
Compare
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
A catalog property typed as, e.g., ["string", "null"] now accepts any of the listed types. Moved here from a2ui-project#215 so the assistant PR no longer changes Gallery code.
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
A catalog property typed as, e.g., ["string", "null"] now accepts any of the listed types. Moved here from a2ui-project#215 so the assistant PR no longer changes Gallery code.
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.
A renderer-owned Slack catalog (samples/react-slack-catalog, served on port 3460) previews A2UI surfaces as Slack Block Kit messages and exports the same validated Block Kit JSON, with no Slack credentials. - 11 Slack components with Gallery examples, native TextField and date-only DateTimeInput controls with writable bindings, aligned tables, diagnostics for approximated layouts, and Block Kit export. - A built-in Book a Car form as real canvas content. - Catalog-aware Gemini/CopilotKit generation and refinement, plus a switchRenderer frontend tool; catalog responses from a detached or replaced renderer cannot overwrite the active catalog. - Slack assets in local, GitHub Pages, and PR-preview builds. - Browser journeys wait for the preview to settle before interacting, so a late render cannot reset a field mid-typing. Built on a2ui-project#215 and independent of the Gallery redesign in a2ui-project#129; the Gallery journey reads examples from main's Gallery.

Gemini + CopilotKit for Composer
Pair Gemini's A2UI generation and editing with CopilotKit's Angular chat interface in Composer's dockable Assistant panel. Users can ask Gemini to change the current design and see the updated canvas alongside readable CopilotKit message cards.
Gemini remains the model behind the assistant. CopilotKit provides the chat presentation, message cards, and prompt composer; Composer's existing services manage model requests, catalog instructions, history, validation, and editor updates. This uses
@copilotkit/angular@0.5.2(MIT) and supersedes #122.The renderer menu that used to be in the prompt pill is now its own PR, #244, so it can be discussed separately.
Two chat panels, chosen through DI
CopilotKit is optional. Composer can be built without it by removing one provider.
ChatPanelBaseholds what both panels share. It is main'sChatPanellogic, unchanged apart fromsubmitPromptrepeating the Send button's checks, because pressing Enter calls it directly.ChatPanelis the dependency-free panel: main's template, styles, and unit spec, unchanged.CopilotKitChatPanelextends the same base and renders it with CopilotKit. It prepares its own view of the history, because CopilotKit renders turns as Markdown and has to show a summary instead of raw canvas JSON; the code comments explain each difference. All CopilotKit code (component, template, styles, theme) lives inchat/copilotkit-chat-panel/, andglobal_styles.scssis unchanged from main.CHAT_PANEL_COMPONENTdecides which one the workspace mounts. It defaults toChatPanel; the open-sourceapp.config.tsprovidesCopilotKitChatPanel. That provider is the only reference to CopilotKit outside its folder, so a build that drops it never imports@copilotkit/angular.FORCE_PLAIN_CHAT_PANEL, a local storage flag next to the existing force-1P/3P flags, makes the app mount the plain panel, so browser tests cover both panels.The CopilotKit panel and the plain panel after the same edit, in light and dark (CopilotKit first in each pair):
Input and editing behavior
These live in the shared services, so both panels get them.
ChatCoordinator.resolveLayoutUpdatedocuments how.The CopilotKit panel adds a single rounded input pill with an Add (+) menu for files, screenshot inclusion, and instructions. Screenshot inclusion has a checked state and an indicator on the Add button, and the menu follows the theme and supports keyboard navigation. The input stays mounted while canvas snapshots refresh, so typed text, focus, and an open menu survive. Attachments, screenshots, instructions, retry, stop, copy controls, and both themes are preserved.
See it in action
Full-resolution MP4. Recorded on
d2c581f, before the rebase onto the latest main, against the local dev servers with a deterministic local Gemini-response fixture: the CopilotKit panel editing a one-Text draft, the Add menu, and dark mode, then the same edit in the plain panel. No live model request or real API key is used.Tests
@axe-core/playwright(a new dev dependency) scans the chat panel against WCAG 2 A and AA in light and dark mode.Validation
Rebased onto the current
main(492122b, including #245's move toErrorLogger, which the shared chat panel base follows); head is3b50ad1.Scope and merge order
This PR doesn't depend on #129 and can merge on its own. #244 (the renderer menu) builds on it, and #216 builds on #244.