-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(web-core): implement client capabilities API in MessageProcessor #826
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
c5972a1
d91acc1
e109ed5
679ebe5
3002c85
761d27e
4dd2332
5ab80eb
eda91f3
9897453
e7bd3ed
0184d18
443f20c
7cc1587
b5210a3
21019fd
d5e7856
bf31f90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ import { Catalog, ComponentApi } from "../catalog/types.js"; | |
| import { SurfaceGroupModel } from "../state/surface-group-model.js"; | ||
| import { ComponentModel } from "../state/component-model.js"; | ||
| import { Subscription } from "../common/events.js"; | ||
| import { zodToJsonSchema } from "zod-to-json-schema"; | ||
|
|
||
| import { | ||
| A2uiMessage, | ||
|
|
@@ -29,6 +30,14 @@ import { | |
| } from "../schema/server-to-client.js"; | ||
| import { A2uiStateError, A2uiValidationError } from "../errors.js"; | ||
|
|
||
| /** | ||
| * Options for generating client capabilities. | ||
| */ | ||
| export interface CapabilitiesOptions { | ||
| /** If true, the full definition of all catalogs will be included. */ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought the idea for this option was to allow inline catalogs, not to include the full definition of all of them.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, for now this API just turns on sending the full capabilities for every catalog. In the future, we might want some more fine-grained API to say "just send full inline catalogs for these three schemas", but I figure this is a simple starting point? WDYT?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I guess start here and we can get more complicated/refined as time goes on. I was just thinking about how it is including these with every message and thinking it was a lot.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SGTM! |
||
| includeInlineCatalogs?: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * The central processor for A2UI messages. | ||
| * @template T The concrete type of the ComponentApi. | ||
|
|
@@ -52,6 +61,100 @@ export class MessageProcessor<T extends ComponentApi> { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Generates the a2uiClientCapabilities object for the current processor. | ||
| * | ||
| * @param options Configuration for capability generation. | ||
| * @returns The capabilities object. | ||
| */ | ||
| getClientCapabilities(options?: CapabilitiesOptions): any { | ||
jacobsimionato marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const capabilities: any = { | ||
| "v0.9": { | ||
| supportedCatalogIds: this.catalogs.map((c) => c.id), | ||
| }, | ||
| }; | ||
|
|
||
| if (options?.includeInlineCatalogs) { | ||
| capabilities["v0.9"].inlineCatalogs = this.catalogs.map((c) => | ||
| this.generateInlineCatalog(c), | ||
| ); | ||
| } | ||
|
|
||
| return capabilities; | ||
| } | ||
|
|
||
| private generateInlineCatalog(catalog: Catalog<T>): any { | ||
jacobsimionato marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const components: Record<string, any> = {}; | ||
|
|
||
| for (const [name, api] of catalog.components.entries()) { | ||
| const zodSchema = zodToJsonSchema(api.schema, { | ||
| target: "jsonSchema2019-09", | ||
| }) as any; | ||
|
|
||
| // Clean up Zod-specific artifacts and process REF: tags | ||
| this.processRefs(zodSchema); | ||
|
|
||
| // Wrap in standard A2UI component envelope (ComponentCommon) | ||
| components[name] = { | ||
| allOf: [ | ||
| { $ref: "common_types.json#/$defs/ComponentCommon" }, | ||
| { | ||
| properties: { | ||
| component: { const: name }, | ||
| ...zodSchema.properties, | ||
| }, | ||
| required: ["component", ...(zodSchema.required || [])], | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| catalogId: catalog.id, | ||
| components, | ||
| }; | ||
| } | ||
|
|
||
| private processRefs(node: any): void { | ||
| if (typeof node !== "object" || node === null) return; | ||
|
|
||
| if (Array.isArray(node)) { | ||
| for (const item of node) { | ||
| this.processRefs(item); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| for (const key of Object.keys(node)) { | ||
| const val = node[key]; | ||
|
|
||
| if (key === "description" && typeof val === "string") { | ||
| if (val.startsWith("REF:")) { | ||
| const parts = val.substring(4).split("|"); | ||
| const ref = parts[0]; | ||
| const desc = parts[1] || ""; | ||
|
|
||
| // Mutate the parent node to be a $ref | ||
| // We remove other validation keywords that Zod might have added (like type: string) | ||
| // but keep the description. | ||
| for (const k of Object.keys(node)) { | ||
| if (k !== "description") { | ||
| delete node[k]; | ||
| } | ||
| } | ||
| node["$ref"] = ref; | ||
| if (desc) { | ||
| node["description"] = desc; | ||
| } else { | ||
| delete node["description"]; | ||
| } | ||
| } | ||
| } else { | ||
| this.processRefs(val); | ||
| } | ||
| } | ||
| } | ||
jacobsimionato marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Subscribes to surface creation events. | ||
| */ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be bloating the front end, though. I was tempted to add this in the past too, but decided against it in order to keep the front end lean. If you think it's worth it, we should quantify the size hit.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, we have decided to use Zod to represent schemas, and the A2UI specification supports inline catalogs, requiring those Zod schemas to be converted to JSON. So I think our choices are:
A. Add the dep
B. Write our own more lightweight version of the same conversion
C. Not support inlineCatalogs at all
D. Support inlineCatalogs through an additional package we provide.
I vote we do A for now, then switch to D if people complain. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, let's add the dep for now. If it's too bloated, we can evalutate B.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM. I noticed that zod 4 has this functionality built into the Zod package natively, so I don't know if there's a benefit from excluding it anyway. I guess we should update to zod 4 at at some point.