Skip to content

Support for version 0.9 of the A2UI protocol in the Angular renderer#820

Closed
gspencergoog wants to merge 11 commits intogoogle:mainfrom
gspencergoog:angular_renderer_core
Closed

Support for version 0.9 of the A2UI protocol in the Angular renderer#820
gspencergoog wants to merge 11 commits intogoogle:mainfrom
gspencergoog:angular_renderer_core

Conversation

@gspencergoog
Copy link
Collaborator

@gspencergoog gspencergoog commented Mar 11, 2026

Description

Overview

This Pull Request introduces initial support for version 0.9 of the A2UI protocol in the Angular renderer and provides updates/strict validations in web_core. This is the first part of a two-part change, with the second part following in the angular_renderer branch. It leaves the samples using v0.8, and a later change will convert them to use 0.9.

There some minimal import changes necessary, which is a breaking change, since v0.9 is now the default, and 0.8 needs to be explicitly imported.

Key Changes

Angular Renderer (renderers/angular)

  • Versioned Organization: Existing v0.8 files are moved and isolated into src/lib/v0_8/ to make room for future proofing.
  • v0.9 Module Support: Created a canonical v0_9/ directory containing:
    • Components: Fresh implementations of v0.9 compliant components (such as audio, button, card, checkbox, choice-picker, column, divider, icon, image, list, modal, slider, surface, tabs, text, text-field, video).
    • Rendering Pipeline: Completely updated dynamic-component, renderer, and id-generator node traversal strategies.
    • Styling Utility: Introduced styles.ts containing ported CSS utility tokens for layout, palette-mapping, and response behaviors.
    • Data Processing: Enhanced context isolation patterns (data-context.ts) and markdown parsing hooks.
  • Default Baseline Setup: public-api.ts now defaults to exporting the v0.9 pipeline set.

Web Core Maintenance (renderers/web_core)

  • Strict Context Validation: Upgraded function handlers inside BASIC_FUNCTIONS (arithmetic, logical operators, string lookups, validations) to explicitly throw A2uiExpressionError on missing or malformed arguments, preventing undefined fallback behavior passes.

Samples & Configurations (samples/client/angular)

  • Client Alignments: Patched node configurations across sample directories (orchestrator, gallery, rizzcharts, contact) to consume the v0.9 renderer exports safely.
  • Rizzcharts Fixes: Included fixes for fallback state behaviors that prevent rendering layout snaps.

Core Logic & Error Handling
- **`renderers/web_core/src/v0_9/basic_catalog/functions/basic_functions.ts`**
  - Updated arithmetic (`add`, `subtract`, `multiply`, `divide`) and comparison (`equals`, `not_equals`, `greater_than`, `less_than`) functions to throw `A2uiExpressionError` when arguments are invalid or missing, replacing previous quiet failures (like returning `NaN`).
  - Added descriptive error messages to aid debugging.

Components & Typings
- **`renderers/web_core/src/v0_9/basic_catalog/components.ts`** **[NEW]**
  - Created a definition file summarizing TypeScript interfaces for standard components like `ButtonProps`, `TextProps`, `ImageProps`, `CardProps`, `RowProps`, `ColumnProps`, `ListNode`, and items. These form the building blocks for creating surfaces in the setup with properties.
- Fixed a2a-chat-canvas resolver to appropriately handle surface styles.
- Fixed orchestrator middleware catalog pass-through to avoid stripping custom capabilities.
- Fixed toolbar catalog sort order to prioritize Custom above Standard model.
- Enabled provideMarkdownRenderer in app config for markdown-it.
- Removed debug console trace logs from chart component views.
…tiply, greater_than, and less_than to match divide behavior
…ypes

Simplifies component Node types (ButtonNode, ImageNode, etc.) by removing explicit property intersections that are already defined in their corresponding Props interfaces, aligning with the pattern used for TextNode.
Copy link
Contributor

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

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 introduces a significant upgrade to the Angular renderer, transitioning from A2UI protocol v0.8 to v0.9. This involves creating new versioned directories (v0_8 and v0_9) for components, catalogs, and related utilities, and updating the main Angular public API to expose the v0.9 implementation. Dependency updates include @a2ui/web_core and the addition of ajv packages. Key changes in v0.9 components include revised input bindings for layout components (Row, Column), updated property names (e.g., usageHint to variant for Text and Image, textFieldType to variant for TextField, selections to value for MultipleChoice, tabItems to tabs for Tabs, minValue/maxValue to min/max for Slider), and a new ChoicePicker component. The DynamicComponent base class in v0.9 now handles component property updates via subscriptions and uses a more robust DataContext for value resolution. The MessageProcessor is updated to handle v0.9 messages and dispatch client events. Additionally, the .gitignore file is updated to include coverage/. Review comments highlight a CSS naming issue in v0_9/components/surface.ts where --0 should be --p-0 for consistency and validity, and a potential bug in v0_9/components/text.ts where String(null) could lead to unintended rendering of the literal string "null" instead of an empty string, suggesting the use of the nullish coalescing operator.

gspencergoog and others added 2 commits March 11, 2026 14:52
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Copy link
Collaborator

@jacobsimionato jacobsimionato left a comment

Choose a reason for hiding this comment

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

Hey I think the v0.9 code here still uses very v0.8 patterns.. I wonder if it's worth vibecoding a new v0.9 renderer from scratch, as an experiment. I tried it out for React here: #827

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is defined in web_core - we should use it from there!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a leftover file that isn't actually used. Removed.

}

@Injectable({ providedIn: 'root' })
export class MessageProcessor extends A2uiMessageProcessor<any> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should also be in web_core!


readonly surfaceId = input<Types.SurfaceID>();
readonly component = input.required<T>();
readonly weight = input.required<string | number>();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is part of the basic catalog, not the spec, so it shouldn't be here

private viewContainerRef = inject(ViewContainerRef);
private catalog = inject(CatalogToken);

private processor = inject(A2UI_PROCESSOR);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should accept just SurfaceModel, not a2uiprocessor overall

}
`,
})
export class Button extends DynamicComponent<Types.ButtonNode> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We want to stop depending on these types and try to completely separate the component implementations from the core renderer codebase.

@gspencergoog
Copy link
Collaborator Author

Hey I think the v0.9 code here still uses very v0.8 patterns.. I wonder if it's worth vibecoding a new v0.9 renderer from scratch, as an experiment. I tried it out for React here: #827

Sure, I can give that a try. This started from the 0.8 source, which is probably why it's using those patterns. I've been trying to cure it of some of the patterns (treating the basic_catalog as separate, making things use web_core), but not entirely successful, as you see.


@Component({
selector: 'a2ui-audio',
changeDetection: ChangeDetectionStrategy.Eager,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not make the components OnPush?

import { MessageProcessor, A2uiClientMessage } from '../data';
import { ComponentModel } from '@a2ui/web_core/v0_9';

let idCounter = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this just be a static private member of the class?

})
export abstract class DynamicComponent<T extends Types.AnyComponentNode = Types.AnyComponentNode> implements OnInit, OnDestroy {
protected readonly id = `a2ui-${idCounter++}`;
protected processor = inject(A2UI_PROCESSOR) as MessageProcessor;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this be readonly ? (Same for cdr.)

export type LinkNode = Component<LinkProps>;
export type Link = LinkNode;

export type CardNode = import('@a2ui/web_core/v0_9/basic_catalog').CardNode;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be in the list abvoe?

observe<T>(path: string): Observable<T> {
const resolvedPath = this.resolvePath(path);
return new Observable<T>((subscriber) => {
// Emit initial value
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we update the comment to explain why we're eating the initial value?

subscriber.next(this.model.get(resolvedPath));

const subscription = this.model.subscribe<T>(resolvedPath, (value) => {
if (value !== undefined) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this mean that if there is a value at the path, and then it gets updated to be undefined, the subscribers won't know the value has been changed?

// The base class processMessages expects v0.9 messages.
// We trust the server sends valid v0.9 messages.
override processMessages(messages: any[]): void {
console.log('[MessageProcessor] Received messages to process:', messages);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should these console.log lines be removed?

expect(component).toBeTruthy();
});

// Tests for resolve/data binding would need to be checked against how DynamicComponent uses them.
Copy link
Collaborator

Choose a reason for hiding this comment

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

These look like LLM-generated comments?

Should we implement additional tests?

},
})
export abstract class DynamicComponent<T extends Types.AnyComponentNode = Types.AnyComponentNode> implements OnInit, OnDestroy {
protected readonly id = `a2ui-${idCounter++}`;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we use getUniqueId() here?

Or should we inject the IdGenerator and use that here and replace the getUniqueId with that?

@gspencergoog
Copy link
Collaborator Author

Sorry, this PR is clearly not ready.

I was excited that I got both v0.8 and v0.9 rendering working, but was premature in sending it out.

Sorry to waste your time with the reviews.

I'll try a clean implementation and see if I can get closer to something correct: I think this one suffers a lot from iteration creep and cruft where I started off with porting the v0.8 implementation to a pure v0.9 implementation, then moved a bunch of that into web_core, and then split things apart again so that it would do both 0.9 and 0.8.

@github-project-automation github-project-automation bot moved this from Todo to Done in A2UI Mar 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants