Skip to content

feat(lit): Initial Draft of v0.9 Lit Renderer and Local Gallery#869

Open
jacobsimionato wants to merge 4 commits intogoogle:mainfrom
jacobsimionato:lit-renderer
Open

feat(lit): Initial Draft of v0.9 Lit Renderer and Local Gallery#869
jacobsimionato wants to merge 4 commits intogoogle:mainfrom
jacobsimionato:lit-renderer

Conversation

@jacobsimionato
Copy link
Collaborator

Description of Changes

This PR introduces the initial draft implementation of the A2UI v0.9 Lit Renderer (@a2ui/lit/v0_9), leveraging the framework-agnostic models from @a2ui/web_core/v0_9.

  • A2uiController: A Lit Reactive Controller that automatically bridges the GenericBinder logic from web core into the native Lit element lifecycle. It manages data subscriptions and triggers requestUpdate() on the host when A2UI data dependencies change.
  • Component Factories: Added createLitComponent for concisely generating pure functional/stateless elements strictly typed via Zod schemas.
  • Catalogs: Full implementation of the v0.9 minimal_catalog and basic_catalog including 18 standard components (Text, Button, Row, Column, etc.) and evaluation functions (capitalize, formatString, arithmetic).
  • Client Capabilities: Enhanced @a2ui/web_core's MessageProcessor to automatically generate a2uiClientCapabilities dynamically from the underlying Zod API schemas, correctly resolving external $ref URIs from the REF: descriptions.
  • Local Gallery: A new agentless Vite workspace at samples/client/lit/gallery_v0_9 featuring an interactive 3-column debugging layout:
    • Spec navigation panel (automatically syncing JSON files directly from the v0.9 specifications).
    • Progressive rendering message stepper to step through payloads incrementally.
    • A live-updating Data Model and Action Log inspector.

Rationale

This aligns the Lit ecosystem with the 'Unified Architecture' concepts drafted in v0.9, ensuring robust separation between the A2UI state protocol (DataModel, SurfaceModel, ComponentContext) and the actual screen painting (DOM templates).

By adopting a Reactive Controller pattern, existing and new Lit components can 'opt-in' to A2UI bindings without requiring inheritance from an obstructive base class. Leveraging Zod schemas enables us to guarantee that the UI implementation remains tightly coupled to the capabilities we broadcast back to the AI Agent.

Testing/Running Instructions

To run the interactive debugger and step through the newly implemented examples locally:

# 1. Build the updated Web Core models and Generic Binder
cd renderers/web_core
npm install && npm run build

# 2. Build the new Lit Renderer architecture
cd ../lit
npm install && npm run build

# 3. Start the Vite gallery application
cd ../../samples/client/lit/gallery_v0_9
npm install && npm run dev

Then visit the logged localhost URL (usually http://localhost:5173) in your browser to view the progressive rendering stepper and live Data Model.

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 new feature: the v0.9 Lit renderer and a local gallery application. The overall architecture, including the A2uiController, component factories, and the separation of concerns between web_core and the lit renderer, is well-thought-out. The new client capabilities generation from Zod schemas is a particularly powerful addition.

My review focuses on improving code quality and maintainability as this feature moves from a draft to a production-ready state. The main points include avoiding inline styles, improving type safety by removing any types and unsafe casts, and refactoring duplicated code. These changes will make the new renderer more robust, easier to theme, and more maintainable in the long run.

import { CardApi } from "@a2ui/web_core/v0_9/basic_catalog";

export const A2uiCard = createLitComponent(CardApi, ({ props, buildChild }) => {
return html`<div class="a2ui-card" style="border: 1px solid #ccc; border-radius: 8px; padding: 16px;">${props.child ? buildChild(props.child) : ""}</div>`;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using inline styles is generally discouraged as it makes theming and maintenance difficult, and can violate Content Security Policy (CSP) in some environments. It would be better to move these styles to a dedicated CSS file or a static styles block within the component, and apply them via classes. This issue is present in several other new components as well (e.g., Column, Row, Divider, Modal, Tabs).

export class A2uiLitModal extends LitElement {
@property({ type: Object }) accessor context!: ComponentContext;
@property({ type: Function }) accessor buildChild!: ChildBuilder;
private a2ui = new A2uiController(this as any, ModalApi);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Casting this to any when creating the A2uiController bypasses TypeScript's type safety. This is often a symptom of the component's type not being fully inferred at the point of property initialization. To improve type safety, consider initializing the controller in the constructor after super() has been called, where this is fully typed. This same issue is also present in A2uiLitTabs.

For example:

export class A2uiLitModal extends LitElement {
  // ... properties
  private a2ui: A2uiController<any>;

  constructor() {
    super();
    this.a2ui = new A2uiController(this, ModalApi);
  }
  // ... rest of the class
}

Comment on lines +7 to +8
const tagMap: Record<string, string> = { h1: "h1", h2: "h2", h3: "h3", h4: "h4", h5: "h5", caption: "span", body: "p" };
const tag = tagMap[variant as string] || "p";
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The tagMap and tag variables are declared, but tag is never used. The component's logic relies on a switch statement instead, making the tagMap and tag declarations redundant. You can remove them to simplify the code.

Comment on lines +28 to +49
function mapJustify(justify: string | undefined): string {
switch (justify) {
case "start": return "flex-start";
case "center": return "center";
case "end": return "flex-end";
case "spaceBetween": return "space-between";
case "spaceAround": return "space-around";
case "spaceEvenly": return "space-evenly";
case "stretch": return "stretch";
default: return "flex-start";
}
}

function mapAlign(align: string | undefined): string {
switch (align) {
case "start": return "flex-start";
case "center": return "center";
case "end": return "flex-end";
case "stretch": return "stretch";
default: return "stretch";
}
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The helper functions mapJustify and mapAlign are duplicated in both Column.ts and Row.ts. To follow the DRY (Don't Repeat Yourself) principle and improve maintainability, these functions should be extracted into a shared utility file.

<div class="a2ui-choicepicker">
${props.label ? html`<label>${props.label}</label>` : ""}
<div class="options">
${props.options?.map((opt: any) => html`
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The type for the opt parameter in the map function is any. This weakens type safety. The type for the options should be inferred from the ChoicePickerApi.schema. Please define a specific type for the options and use it here to leverage TypeScript's static analysis.


export const A2uiImage = createLitComponent(ImageApi, ({ props }) => {
const styles = { objectFit: props.fit || "fill", width: "100%" };
return html`<img src=${props.url} class=${"a2ui-image " + (props.variant || "")} style=${styleMap(styles)} />`;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using string concatenation for CSS classes is less robust than using the classMap directive from Lit. classMap handles conditional classes more cleanly and avoids potential issues with extra spaces or empty class attributes.

import { classMap } from "lit/directives/class-map.js";
// ...
export const A2uiImage = createLitComponent(ImageApi, ({ props }) => {
  const styles = { objectFit: props.fit || "fill", width: "100%" };
  const classes = { 
    'a2ui-image': true,
    [props.variant || '']: !!props.variant
  };
  return html`<img src=${props.url} class=${classMap(classes)} style=${styleMap(styles)} />`;
});

@ditman
Copy link
Collaborator

ditman commented Mar 17, 2026

(I'm stealing all of this :P)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

2 participants