feat(macos): add Canvas A2UI renderer

This commit is contained in:
Peter Steinberger
2025-12-17 11:35:06 +01:00
parent 1cdebb68a0
commit cdb5ddb2da
408 changed files with 73598 additions and 32 deletions

View File

@@ -51,6 +51,7 @@ let package = Package(
resources: [
.copy("Resources/Clawdis.icns"),
.copy("Resources/WebChat"),
.copy("Resources/CanvasA2UI"),
],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency"),

View File

@@ -28,20 +28,17 @@ final class CanvasManager {
.trimmingCharacters(in: .whitespacesAndNewlines)
.nonEmpty
let isWebTarget = Self.isWebTarget(normalizedTarget)
if let controller = self.panelController, self.panelSessionKey == session {
controller.presentAnchoredPanel(anchorProvider: anchorProvider)
controller.applyPreferredPlacement(placement)
// Existing session: only navigate when an explicit target was provided.
if let normalizedTarget {
controller.goto(path: normalizedTarget)
controller.load(target: normalizedTarget)
return self.makeShowResult(
directory: controller.directoryPath,
target: target,
effectiveTarget: normalizedTarget,
isWebTarget: isWebTarget)
effectiveTarget: normalizedTarget)
}
return CanvasShowResult(
@@ -72,8 +69,7 @@ final class CanvasManager {
return self.makeShowResult(
directory: controller.directoryPath,
target: target,
effectiveTarget: effectiveTarget,
isWebTarget: isWebTarget)
effectiveTarget: effectiveTarget)
}
func hide(sessionKey: String) {
@@ -111,18 +107,29 @@ final class CanvasManager {
// MARK: - Helpers
private static func isWebTarget(_ target: String?) -> Bool {
guard let target, let url = URL(string: target), let scheme = url.scheme?.lowercased() else { return false }
return scheme == "https" || scheme == "http"
private static func directURL(for target: String?) -> URL? {
guard let target else { return nil }
let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
if let url = URL(string: trimmed), let scheme = url.scheme?.lowercased() {
if scheme == "https" || scheme == "http" || scheme == "file" { return url }
}
// Convenience: existing absolute paths resolve as local files.
if trimmed.hasPrefix("/"), FileManager.default.fileExists(atPath: trimmed) {
return URL(fileURLWithPath: trimmed)
}
return nil
}
private func makeShowResult(
directory: String,
target: String?,
effectiveTarget: String,
isWebTarget: Bool) -> CanvasShowResult
effectiveTarget: String) -> CanvasShowResult
{
if isWebTarget, let url = URL(string: effectiveTarget) {
if let url = Self.directURL(for: effectiveTarget) {
return CanvasShowResult(
directory: directory,
target: target,
@@ -151,12 +158,12 @@ final class CanvasManager {
if path.hasPrefix("/") { path.removeFirst() }
path = path.removingPercentEncoding ?? path
// Root special-case: welcome page when no index exists.
// Root special-case: built-in shell page when no index exists.
if path.isEmpty {
let a = sessionDir.appendingPathComponent("index.html", isDirectory: false)
let b = sessionDir.appendingPathComponent("index.htm", isDirectory: false)
if fm.fileExists(atPath: a.path) || fm.fileExists(atPath: b.path) { return .ok }
return .welcome
return Self.hasBundledA2UIShell() ? .a2uiShell : .welcome
}
// Direct file or directory.
@@ -187,6 +194,14 @@ final class CanvasManager {
let b = dir.appendingPathComponent("index.htm", isDirectory: false)
return fm.fileExists(atPath: b.path)
}
private static func hasBundledA2UIShell() -> Bool {
guard let base = Bundle.module.resourceURL?.appendingPathComponent("CanvasA2UI", isDirectory: true) else {
return false
}
let index = base.appendingPathComponent("index.html", isDirectory: false)
return FileManager.default.fileExists(atPath: index.path)
}
}
private extension String {

View File

@@ -7,6 +7,8 @@ private let canvasLogger = Logger(subsystem: "com.steipete.clawdis", category: "
final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
private let root: URL
private static let builtinPrefix = "__clawdis__/a2ui"
init(root: URL) {
self.root = root
}
@@ -64,6 +66,10 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
if path.hasPrefix("/") { path.removeFirst() }
path = path.removingPercentEncoding ?? path
if let builtin = self.builtinResponse(requestPath: path) {
return builtin
}
// Special-case: welcome page when root index is missing.
if path.isEmpty {
let indexA = sessionRoot.appendingPathComponent("index.html", isDirectory: false)
@@ -71,7 +77,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
if !FileManager.default.fileExists(atPath: indexA.path),
!FileManager.default.fileExists(atPath: indexB.path)
{
return self.welcomePage(sessionRoot: sessionRoot)
return self.a2uiShellPage(sessionRoot: sessionRoot)
}
}
@@ -197,6 +203,54 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
return self.html(body, title: "Canvas")
}
private func a2uiShellPage(sessionRoot: URL) -> CanvasResponse {
// Default Canvas UX: when no index exists, show the built-in A2UI shell.
if let data = self.loadBundledResourceData(subdirectory: "CanvasA2UI", relativePath: "index.html") {
return CanvasResponse(mime: "text/html", data: data)
}
// Fallback for dev misconfiguration: show the classic welcome page.
return self.welcomePage(sessionRoot: sessionRoot)
}
private func builtinResponse(requestPath: String) -> CanvasResponse? {
let trimmed = requestPath.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
guard trimmed == Self.builtinPrefix
|| trimmed == Self.builtinPrefix + "/"
|| trimmed.hasPrefix(Self.builtinPrefix + "/")
else { return nil }
let relative: String
if trimmed == Self.builtinPrefix || trimmed == Self.builtinPrefix + "/" {
relative = "index.html"
} else {
relative = String(trimmed.dropFirst((Self.builtinPrefix + "/").count))
}
if relative.isEmpty { return self.html("Not Found", title: "Canvas: 404") }
if relative.contains("..") || relative.contains("\\") {
return self.html("Forbidden", title: "Canvas: 403")
}
guard let data = self.loadBundledResourceData(subdirectory: "CanvasA2UI", relativePath: relative) else {
return self.html("Not Found", title: "Canvas: 404")
}
let ext = (relative as NSString).pathExtension
let mime = CanvasScheme.mimeType(forExtension: ext)
return CanvasResponse(mime: mime, data: data)
}
private func loadBundledResourceData(subdirectory: String, relativePath: String) -> Data? {
guard let base = Bundle.module.resourceURL?.appendingPathComponent(subdirectory, isDirectory: true) else {
return nil
}
let url = base.appendingPathComponent(relativePath, isDirectory: false)
return try? Data(contentsOf: url)
}
private func textEncodingName(forMimeType mimeType: String) -> String? {
if mimeType.hasPrefix("text/") { return "utf-8" }
switch mimeType {

View File

@@ -100,7 +100,7 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
if case let .panel(anchorProvider) = self.presentation {
self.presentAnchoredPanel(anchorProvider: anchorProvider)
if let path {
self.goto(path: path)
self.load(target: path)
}
return
}
@@ -109,7 +109,7 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
self.window?.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
if let path {
self.goto(path: path)
self.load(target: path)
}
self.onVisibilityChanged?(true)
}
@@ -124,14 +124,27 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
self.onVisibilityChanged?(false)
}
func goto(path: String) {
let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
func load(target: String) {
let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines)
if let url = URL(string: trimmed), let scheme = url.scheme?.lowercased(),
scheme == "https" || scheme == "http"
{
canvasWindowLogger.debug("canvas goto web \(url.absoluteString, privacy: .public)")
self.webView.load(URLRequest(url: url))
if let url = URL(string: trimmed), let scheme = url.scheme?.lowercased() {
if scheme == "https" || scheme == "http" {
canvasWindowLogger.debug("canvas load url \(url.absoluteString, privacy: .public)")
self.webView.load(URLRequest(url: url))
return
}
if scheme == "file" {
canvasWindowLogger.debug("canvas load file \(url.absoluteString, privacy: .public)")
self.loadFile(url)
return
}
}
// Convenience: absolute paths resolve as local files when they exist.
if trimmed.hasPrefix("/"), FileManager.default.fileExists(atPath: trimmed) {
let url = URL(fileURLWithPath: trimmed)
canvasWindowLogger.debug("canvas load file \(url.absoluteString, privacy: .public)")
self.loadFile(url)
return
}
@@ -144,10 +157,16 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
"invalid canvas url session=\(self.sessionKey, privacy: .public) path=\(trimmed, privacy: .public)")
return
}
canvasWindowLogger.debug("canvas goto canvas \(url.absoluteString, privacy: .public)")
canvasWindowLogger.debug("canvas load canvas \(url.absoluteString, privacy: .public)")
self.webView.load(URLRequest(url: url))
}
private func loadFile(_ url: URL) {
let fileURL = url.isFileURL ? url : URL(fileURLWithPath: url.path)
let accessDir = fileURL.deletingLastPathComponent()
self.webView.loadFileURL(fileURL, allowingReadAccessTo: accessDir)
}
func eval(javaScript: String) async -> String {
await withCheckedContinuation { cont in
self.webView.evaluateJavaScript(javaScript) { result, error in

View File

@@ -67,6 +67,9 @@ enum ControlRequestHandler {
case let .canvasSnapshot(session, outPath):
return await self.handleCanvasSnapshot(session: session, outPath: outPath)
case let .canvasA2UI(session, command, jsonl):
return await self.handleCanvasA2UI(session: session, command: command, jsonl: jsonl)
case .nodeList:
return await self.handleNodeList()
@@ -228,6 +231,86 @@ enum ControlRequestHandler {
}
}
private static func handleCanvasA2UI(session: String, command: CanvasA2UICommand, jsonl: String?) async -> Response {
guard self.canvasEnabled() else { return Response(ok: false, message: "Canvas disabled by user") }
do {
// Ensure the Canvas is visible and the default page is loaded.
_ = try await MainActor.run {
try CanvasManager.shared.show(sessionKey: session, path: "/")
}
let ready = await Self.waitForCanvasA2UI(session: session, timeoutMs: 2_000)
guard ready else { return Response(ok: false, message: "A2UI not ready") }
let js: String
switch command {
case .reset:
js = """
(() => {
if (!globalThis.clawdisA2UI) { return "missing clawdisA2UI"; }
globalThis.clawdisA2UI.reset();
return "ok";
})()
"""
case .pushJSONL:
guard let jsonl, !jsonl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return Response(ok: false, message: "missing jsonl")
}
let messages: [Any]
do {
messages = try Self.parseJSONL(jsonl)
} catch {
return Response(ok: false, message: "invalid jsonl: \(error.localizedDescription)")
}
let data = try JSONSerialization.data(withJSONObject: messages, options: [])
let json = String(data: data, encoding: .utf8) ?? "[]"
js = """
(() => {
if (!globalThis.clawdisA2UI) { return "missing clawdisA2UI"; }
const messages = \(json);
globalThis.clawdisA2UI.applyMessages(messages);
return "ok";
})()
"""
}
let result = try await CanvasManager.shared.eval(sessionKey: session, javaScript: js)
return Response(ok: true, payload: Data(result.utf8))
} catch {
return Response(ok: false, message: error.localizedDescription)
}
}
private static func parseJSONL(_ text: String) throws -> [Any] {
var out: [Any] = []
for rawLine in text.split(whereSeparator: \.isNewline) {
let line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines)
if line.isEmpty { continue }
let data = Data(line.utf8)
let obj = try JSONSerialization.jsonObject(with: data, options: [])
out.append(obj)
}
return out
}
private static func waitForCanvasA2UI(session: String, timeoutMs: Int) async -> Bool {
let clock = ContinuousClock()
let deadline = clock.now.advanced(by: .milliseconds(timeoutMs))
while clock.now < deadline {
do {
let res = try await CanvasManager.shared.eval(
sessionKey: session,
javaScript: "(() => globalThis.clawdisA2UI ? 'ready' : '')()")
if res == "ready" { return true }
} catch {
// Ignore; keep waiting.
}
try? await Task.sleep(nanoseconds: 60_000_000)
}
return false
}
private static func handleNodeList() async -> Response {
let ids = await BridgeServer.shared.connectedNodeIds()
let payload = (try? JSONSerialization.data(

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,197 @@
import { html, css, LitElement } from "lit";
import { repeat } from "lit/directives/repeat.js";
import { ContextProvider } from "@lit/context";
import { v0_8 } from "@a2ui/lit";
import "@a2ui/lit/ui";
import { themeContext } from "@clawdis/a2ui-theme-context";
const empty = Object.freeze({});
const emptyClasses = () => ({});
const textHintStyles = () => ({ h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, body: {}, caption: {} });
const clawdisTheme = {
components: {
AudioPlayer: emptyClasses(),
Button: emptyClasses(),
Card: emptyClasses(),
Column: emptyClasses(),
CheckBox: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
DateTimeInput: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
Divider: emptyClasses(),
Image: {
all: emptyClasses(),
icon: emptyClasses(),
avatar: emptyClasses(),
smallFeature: emptyClasses(),
mediumFeature: emptyClasses(),
largeFeature: emptyClasses(),
header: emptyClasses(),
},
Icon: emptyClasses(),
List: emptyClasses(),
Modal: { backdrop: emptyClasses(), element: emptyClasses() },
MultipleChoice: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
Row: emptyClasses(),
Slider: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
Tabs: { container: emptyClasses(), element: emptyClasses(), controls: { all: emptyClasses(), selected: emptyClasses() } },
Text: {
all: emptyClasses(),
h1: emptyClasses(),
h2: emptyClasses(),
h3: emptyClasses(),
h4: emptyClasses(),
h5: emptyClasses(),
caption: emptyClasses(),
body: emptyClasses(),
},
TextField: { container: emptyClasses(), element: emptyClasses(), label: emptyClasses() },
Video: emptyClasses(),
},
elements: {
a: emptyClasses(),
audio: emptyClasses(),
body: emptyClasses(),
button: emptyClasses(),
h1: emptyClasses(),
h2: emptyClasses(),
h3: emptyClasses(),
h4: emptyClasses(),
h5: emptyClasses(),
iframe: emptyClasses(),
input: emptyClasses(),
p: emptyClasses(),
pre: emptyClasses(),
textarea: emptyClasses(),
video: emptyClasses(),
},
markdown: {
p: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
ul: [],
ol: [],
li: [],
a: [],
strong: [],
em: [],
},
additionalStyles: {
Card: {
background: "linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03))",
border: "1px solid rgba(255,255,255,.09)",
borderRadius: "14px",
padding: "14px",
boxShadow: "0 10px 30px rgba(0,0,0,.35)",
},
Column: { gap: "10px" },
Row: { gap: "10px", alignItems: "center" },
Divider: { opacity: "0.25" },
Button: {
background: "linear-gradient(135deg, #22c55e 0%, #06b6d4 100%)",
border: "0",
borderRadius: "12px",
padding: "10px 14px",
color: "#071016",
fontWeight: "650",
cursor: "pointer",
boxShadow: "0 10px 25px rgba(6, 182, 212, 0.18)",
},
Text: {
...textHintStyles(),
h1: { fontSize: "20px", fontWeight: "750", margin: "0 0 6px 0" },
h2: { fontSize: "16px", fontWeight: "700", margin: "0 0 6px 0" },
body: { fontSize: "13px", lineHeight: "1.4" },
caption: { opacity: "0.8" },
},
TextField: { display: "grid", gap: "6px" },
Image: { borderRadius: "12px" },
},
};
class ClawdisA2UIHost extends LitElement {
static properties = {
surfaces: { state: true },
};
#processor = v0_8.Data.createSignalA2uiMessageProcessor();
#themeProvider = new ContextProvider(this, {
context: themeContext,
initialValue: clawdisTheme,
});
surfaces = [];
static styles = css`
:host {
display: block;
height: 100%;
box-sizing: border-box;
padding: 12px;
}
#surfaces {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
height: 100%;
overflow: auto;
padding-bottom: 24px;
}
`;
connectedCallback() {
super.connectedCallback();
globalThis.clawdisA2UI = {
applyMessages: (messages) => this.applyMessages(messages),
reset: () => this.reset(),
getSurfaces: () => Array.from(this.#processor.getSurfaces().keys()),
};
this.#syncSurfaces();
}
applyMessages(messages) {
if (!Array.isArray(messages)) {
throw new Error("A2UI: expected messages array");
}
this.#processor.processMessages(messages);
this.#syncSurfaces();
return { ok: true, surfaces: this.surfaces.map(([id]) => id) };
}
reset() {
this.#processor.clearSurfaces();
this.#syncSurfaces();
return { ok: true };
}
#syncSurfaces() {
this.surfaces = Array.from(this.#processor.getSurfaces().entries());
}
render() {
if (this.surfaces.length === 0) {
return html`<div style="opacity:.8; padding: 10px;">
<div style="font-weight: 700; margin-bottom: 6px;">Canvas (A2UI)</div>
<div>Waiting for A2UI messages…</div>
</div>`;
}
return html`<section id="surfaces">
${repeat(
this.surfaces,
([surfaceId]) => surfaceId,
([surfaceId, surface]) => html`<a2ui-surface
.surfaceId=${surfaceId}
.surface=${surface}
.processor=${this.#processor}
></a2ui-surface>`
)}
</section>`;
}
}
customElements.define("clawdis-a2ui-host", ClawdisA2UIHost);

View File

@@ -0,0 +1,24 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Canvas</title>
<style>
:root { color-scheme: light dark; }
html, body { height: 100%; margin: 0; }
body {
font: 13px -apple-system, system-ui;
background: #0b1020;
color: #e5e7eb;
overflow: hidden;
}
clawdis-a2ui-host { display: block; height: 100%; }
</style>
</head>
<body>
<clawdis-a2ui-host></clawdis-a2ui-host>
<script type="module" src="/__clawdis__/a2ui/a2ui.bundle.js"></script>
</body>
</html>

View File

@@ -0,0 +1,33 @@
import path from "node:path";
import { defineConfig } from "rolldown";
const here = path.dirname(new URL(import.meta.url).pathname);
const repoRoot = path.resolve(here, "../../../../../..");
const fromHere = (p) => path.resolve(here, p);
const a2uiLitDist = path.resolve(repoRoot, "vendor/a2ui/renderers/lit/dist/src");
const a2uiThemeContext = path.resolve(a2uiLitDist, "0.8/ui/context/theme.js");
export default defineConfig({
input: fromHere("bootstrap.js"),
treeshake: false,
resolve: {
alias: {
"@a2ui/lit": path.resolve(a2uiLitDist, "index.js"),
"@a2ui/lit/ui": path.resolve(a2uiLitDist, "0.8/ui/ui.js"),
"@clawdis/a2ui-theme-context": a2uiThemeContext,
"@lit/context": path.resolve(repoRoot, "node_modules/@lit/context/index.js"),
"@lit/context/": path.resolve(repoRoot, "node_modules/@lit/context/"),
"@lit-labs/signals": path.resolve(repoRoot, "node_modules/@lit-labs/signals/index.js"),
"@lit-labs/signals/": path.resolve(repoRoot, "node_modules/@lit-labs/signals/"),
lit: path.resolve(repoRoot, "node_modules/lit/index.js"),
"lit/": path.resolve(repoRoot, "node_modules/lit/"),
},
},
output: {
file: fromHere("a2ui.bundle.js"),
format: "esm",
inlineDynamicImports: true,
sourcemap: false,
},
});

View File

@@ -248,6 +248,8 @@ struct ClawdisCLI {
return ParsedCLIRequest(
request: .canvasShow(session: session, path: target, placement: placement),
kind: .generic)
case "a2ui":
return try self.parseCanvasA2UI(args: &args)
case "hide":
var session = "main"
while !args.isEmpty {
@@ -288,6 +290,44 @@ struct ClawdisCLI {
}
}
private static func parseCanvasA2UI(args: inout [String]) throws -> ParsedCLIRequest {
guard let sub = args.popFirst() else { throw CLIError.help }
switch sub {
case "push":
var session = "main"
var jsonlPath: String?
while !args.isEmpty {
let arg = args.removeFirst()
switch arg {
case "--session": session = args.popFirst() ?? session
case "--jsonl": jsonlPath = args.popFirst()
default: break
}
}
guard let jsonlPath else { throw CLIError.help }
let jsonl = try String(contentsOfFile: jsonlPath, encoding: .utf8)
return ParsedCLIRequest(
request: .canvasA2UI(session: session, command: .pushJSONL, jsonl: jsonl),
kind: .generic)
case "reset":
var session = "main"
while !args.isEmpty {
let arg = args.removeFirst()
switch arg {
case "--session": session = args.popFirst() ?? session
default: break
}
}
return ParsedCLIRequest(
request: .canvasA2UI(session: session, command: .reset, jsonl: nil),
kind: .generic)
default:
throw CLIError.help
}
}
private static func parseCamera(args: inout [String]) throws -> ParsedCLIRequest {
guard let sub = args.popFirst() else { throw CLIError.help }
switch sub {
@@ -473,8 +513,10 @@ struct ClawdisCLI {
clawdis-mac node invoke --node <id> --command <name> [--params-json <json>]
Canvas:
clawdis-mac canvas show [--session <key>] [--target </...|https://...>]
clawdis-mac canvas show [--session <key>] [--target </...|https://...|file://...>]
[--x <screenX> --y <screenY>] [--width <w> --height <h>]
clawdis-mac canvas a2ui push --jsonl <path> [--session <key>]
clawdis-mac canvas a2ui reset [--session <key>]
clawdis-mac canvas hide [--session <key>]
clawdis-mac canvas eval --js <code> [--session <key>]
clawdis-mac canvas snapshot [--out <path>] [--session <key>]

View File

@@ -60,13 +60,15 @@ public struct CanvasPlacement: Codable, Sendable {
public enum CanvasShowStatus: String, Codable, Sendable {
/// Panel was shown, but no navigation occurred (no target passed and session already existed).
case shown
/// Target was an http(s) URL.
/// Target was a direct URL (http(s) or file).
case web
/// Local canvas target resolved to an existing file.
case ok
/// Local canvas target did not resolve to a file (404 page).
case notFound
/// Local canvas root ("/") has no index, so the welcome page is shown.
/// Local canvas root ("/") has no index, so the built-in A2UI shell is shown.
case a2uiShell
/// Legacy fallback when the built-in shell isn't available (dev misconfiguration).
case welcome
}
@@ -96,6 +98,13 @@ public struct CanvasShowResult: Codable, Sendable {
}
}
// MARK: - Canvas A2UI
public enum CanvasA2UICommand: String, Codable, Sendable {
case pushJSONL
case reset
}
public enum Request: Sendable {
case notify(
title: String,
@@ -117,6 +126,7 @@ public enum Request: Sendable {
case canvasHide(session: String)
case canvasEval(session: String, javaScript: String)
case canvasSnapshot(session: String, outPath: String?)
case canvasA2UI(session: String, command: CanvasA2UICommand, jsonl: String?)
case nodeList
case nodeInvoke(nodeId: String, command: String, paramsJSON: String?)
case cameraSnap(facing: CameraFacing?, maxWidth: Int?, quality: Double?, outPath: String?)
@@ -151,6 +161,8 @@ extension Request: Codable {
case path
case javaScript
case outPath
case canvasA2UICommand
case jsonl
case facing
case maxWidth
case quality
@@ -173,6 +185,7 @@ extension Request: Codable {
case canvasHide
case canvasEval
case canvasSnapshot
case canvasA2UI
case nodeList
case nodeInvoke
case cameraSnap
@@ -237,6 +250,12 @@ extension Request: Codable {
try container.encode(session, forKey: .session)
try container.encodeIfPresent(outPath, forKey: .outPath)
case let .canvasA2UI(session, command, jsonl):
try container.encode(Kind.canvasA2UI, forKey: .type)
try container.encode(session, forKey: .session)
try container.encode(command, forKey: .canvasA2UICommand)
try container.encodeIfPresent(jsonl, forKey: .jsonl)
case .nodeList:
try container.encode(Kind.nodeList, forKey: .type)
@@ -321,6 +340,12 @@ extension Request: Codable {
let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
self = .canvasSnapshot(session: session, outPath: outPath)
case .canvasA2UI:
let session = try container.decode(String.self, forKey: .session)
let command = try container.decode(CanvasA2UICommand.self, forKey: .canvasA2UICommand)
let jsonl = try container.decodeIfPresent(String.self, forKey: .jsonl)
self = .canvasA2UI(session: session, command: command, jsonl: jsonl)
case .nodeList:
self = .nodeList

View File

@@ -161,5 +161,13 @@ struct ControlRequestHandlerTests {
}
#expect(snap.ok == false)
#expect(snap.message == "Canvas disabled by user")
let a2ui = try await Self.withDefaultOverride(pauseDefaultsKey, value: false) {
try await Self.withDefaultOverride(canvasEnabledKey, value: false) {
try await ControlRequestHandler.process(request: .canvasA2UI(session: "s", command: .reset, jsonl: nil))
}
}
#expect(a2ui.ok == false)
#expect(a2ui.message == "Canvas disabled by user")
}
}

View File

@@ -39,9 +39,17 @@ Routing model:
Directory listings are not served.
When `/` has no `index.html` yet, the handler serves a built-in welcome page with:
- The resolved on-disk session directory path.
- A short “create index.html” hint.
When `/` has no `index.html` yet, the handler serves a **built-in A2UI shell** (bundled with the macOS app).
This gives the agent a ready-to-render UI surface without requiring any on-disk HTML.
If the A2UI shell resources are missing (dev misconfiguration), Canvas falls back to a simple built-in welcome page.
### Reserved built-in paths
The scheme handler serves bundled assets under:
- `clawdis-canvas://<session>/__clawdis__/a2ui/...`
This is reserved for app-owned assets (not session content) and is backed by `Bundle.module` resources.
### Suggested on-disk location
@@ -82,6 +90,26 @@ This should be modeled after `WebChatManager`/`WebChatWindowController` but targ
Related:
- For “invoke the agent again from UI” flows, prefer the macOS deep link scheme (`clawdis://agent?...`) so *any* UI surface (Canvas, WebChat, native views) can trigger a new agent run. See `docs/clawdis-mac.md`.
## Agent commands (current)
`clawdis-mac` exposes Canvas via the control socket. For agent use, prefer `--json` so you can read the structured `CanvasShowResult` (including `status`).
- `clawdis-mac canvas show [--session <key>] [--target <...>] [--x/--y/--width/--height]`
- Local targets map into the session directory via the custom scheme (directory targets resolve `index.html|index.htm`).
- If `/` has no index file, Canvas shows the built-in A2UI shell and returns `status: "a2uiShell"`.
- `clawdis-mac canvas hide [--session <key>]`
- `clawdis-mac canvas eval --js <code> [--session <key>]`
- `clawdis-mac canvas snapshot [--out <path>] [--session <key>]`
### Canvas A2UI
Canvas includes a built-in A2UI renderer (Lit-based). The agent can drive it with JSONL “message” objects:
- `clawdis-mac canvas a2ui push --jsonl <path> [--session <key>]`
- `clawdis-mac canvas a2ui reset [--session <key>]`
`push` expects a JSONL file where **each line is a single JSON object** (parsed and forwarded to the in-page A2UI renderer).
## Triggering agent runs from Canvas (deep links)
Canvas can trigger new agent runs via the macOS app deep-link scheme:

94
vendor/a2ui/.gemini/GEMINI.md vendored Normal file
View File

@@ -0,0 +1,94 @@
# A2UI Gemini Agent Guide
This document serves as a guide for using the Gemini agent within the A2UI repository. It outlines the repository's structure, explains the core concepts of the A2UI protocol, and provides instructions for running the various demos and keeping this guide up-to-date.
## Repository Structure
The A2UI repository is organized into several key directories:
- `specification/0.8/docs/`: Contains the primary human-readable documentation for the A2UI protocol.
- `a2ui_protocol.md`: The foundational specification document. This is the best place to start to understand the protocol's fundamental goals.
- `specification/0.8/json/`: Contains the formal JSON schema definitions for the protocol.
- `server_to_client.json`: Defines the schema for messages sent from the server to the client.
- `client_to_server.json`: Defines the schema for event messages sent from the client to the server.
- `a2a_agents/python/`: Contains Python code relating to server-side integration of A2UI
- `a2ui_extension/`: Python implementation of the A2UI A2A extension.
- `adk/samples/`: Contains demo applications that showcase the A2UI protocol in action using the ADK framework.
- `web/`: Contains the web-based client implementations (using Lit and Vite) for the samples, including a shared library (`renderers/lit`).
- `angular/`: Contains an alternative web-based client implementation using Angular.
- `eval/`: Contains a Genkit-based framework for evaluating LLM performance in generating A2UI responses.
## A2UI Specification Overview
The A2UI protocol is a JSONL-based, streaming UI protocol designed to be easily generated by Large Language Models (LLMs). It enables a server to stream a platform-agnostic, abstract UI definition to a client, which then renders it progressively using a native widget set.
### Core Concepts
The core concepts of the A2UI protocol are detailed in the main specification document. Rather than duplicating the content here, you should refer to the authoritative source:
- **A2UI Protocol Specification**: `@docs/a2ui_protocol.md`
This document covers the design philosophy, architecture, data flow, and core concepts of the protocol.
### Schemas
The formal, machine-readable definitions of the protocol are maintained as JSON schemas:
- **Server-to-Client Schema**: `@specification/0.8/json/server_to_client.json`
- **Server-to-Client Schema, with standard catalog**: `@specification/0.8/json/server_to_client_with_standard_catalog.json`
- **Client-to-Server Schema**: `@specification/0.8/json/client_to_server.json`
## Running the Demos
There are three demos available in the `a2a_samples/` directory. Each demo has a corresponding web client in the `web/` and `angular/` directories. To run a demo, you will need to start both the server and the client.
### Running a Demo Server
To run a demo server, navigate to the demo's directory and run the `__main__.py` script. For example, to run the contact lookup demo:
```bash
cd a2a_samples/a2ui_contact_lookup
python -m __main__
```
### Running a Demo Client (Lit)
To run a demo client, navigate to the corresponding client directory in `web/` and start the development server. For example, to run the contact lookup client:
```bash
cd web/contact
npm install
npm run dev
```
### Running a Demo Client (Angular)
To run a demo client, navigate to the `angular/` directory and start the development server with the project name. For example, to run the contact lookup client:
```bash
cd angular
npm install
npm start -- contact
```
## Renderers
There are three renderers available for A2UI:
- **Web (Lit)**: Located in `renderers/lit`, this is the primary web renderer used by the demos in `web/`.
- **Angular**: Located in `angular/projects/lib`, this is an alternative web renderer for Angular applications.
- **Flutter**: The Flutter renderer is in a separate repository: [https://github.com/flutter/genui](https://github.com/flutter/genui)
## Keeping This Guide Updated
This document is intended to be a living guide for the repository. As the repository evolves, it's important to keep this file up-to-date. When making changes to the repository, please consider the following:
- **New Demos or Clients**: If you add a new demo or client, add it to the "Running the Demos" section.
- **Specification Changes**: If you make significant changes to the A2UI protocol, ensure that the "A2UI Specification Overview" section is updated to reflect the changes, and that any linked documents are also updated.
- **Repository Structure Changes**: If you change the directory structure of the repository, update the "Repository Structure" section.
To get this file back in sync, you can run the following commands:
1. List all the files in the entire repo with `git ls-tree main --name-only -r`
2. Read the ~50 most important files in the list, potentially in batches.
3. Update this file.

79
vendor/a2ui/.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,79 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Docs Build and Deploy
on:
push:
branches:
- main
paths:
- ".github/workflows/docs.yml"
- "requirements-docs.txt"
- "mkdocs.yml"
- "docs/**"
pull_request:
branches:
- main
paths:
- ".github/workflows/docs.yml"
- "requirements-docs.txt"
- "mkdocs.yml"
- "docs/**"
jobs:
build_and_deploy:
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
if: github.repository == 'google/A2UI'
steps:
- name: Checkout Code
uses: actions/checkout@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Configure Git Credentials
run: |
git config --global user.name github-actions[bot]
git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.13
- name: Restore pip cache
uses: actions/cache@v4
with:
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-docs.txt') }}
path: ~/.cache/pip
restore-keys: |
${{ runner.os }}-pip-
- name: Install documentation dependencies
run: pip install -r requirements-docs.txt
- name: Build Documentation (PR Check)
if: github.event_name == 'pull_request'
run: mkdocs build
- name: Deploy development version from main branch
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
mkdocs gh-deploy

View File

@@ -0,0 +1,55 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Editor build
on:
push:
paths:
- 'tools/editor/**'
- 'renderers/lit/**'
- '.github/workflows/editor_build.yml'
pull_request:
paths:
- 'tools/editor/**'
- 'renderers/lit/**'
- '.github/workflows/editor_build.yml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install lib's deps
working-directory: ./renderers/lit
run: npm ci
- name: Build lib
working-directory: ./renderers/lit
run: npm run build
- name: Install editor deps
working-directory: ./tools/editor
run: npm install
- name: Build editor
working-directory: ./tools/editor
run: npm run build

View File

@@ -0,0 +1,56 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Inspector build
on:
push:
branches: [ main ]
paths:
- 'tools/inspector/**'
- 'renderers/lit/**'
- '.github/workflows/inspector_build.yml'
pull_request:
paths:
- 'tools/inspector/**'
- 'renderers/lit/**'
- '.github/workflows/inspector_build.yml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install lib's deps
working-directory: ./renderers/lit
run: npm ci
- name: Build lib
working-directory: ./renderers/lit
run: npm run build
- name: Install inspector deps
working-directory: ./tools/inspector
run: npm install
- name: Build inspector
working-directory: ./tools/inspector
run: npm run build

View File

@@ -0,0 +1,48 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Java sample build and test
on:
push:
branches:
- main
paths:
- 'a2a_agents/java/**'
pull_request:
paths:
- 'a2a_agents/java/**'
jobs:
build-and-test:
name: Build and test Java agent sample
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: Build with Maven
working-directory: a2a_agents/java
run: mvn clean install
- name: Run Tests
working-directory: a2a_agents/java
run: mvn test

View File

@@ -0,0 +1,54 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Lit samples build
on:
push:
branches: [ main ]
paths-ignore:
- 'samples/agent/adk/**'
pull_request:
paths-ignore:
- 'samples/agent/adk/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install lib's deps
working-directory: ./renderers/lit
run: npm i
- name: Build lib
working-directory: ./renderers/lit
run: npm run build
- name: Install all lit samples workspaces' dependencies
working-directory: ./samples/client/lit
run: npm install --workspaces
- name: Build all lit samples workspaces
working-directory: ./samples/client/lit
run: npm run build --workspaces

View File

@@ -0,0 +1,72 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Angular build and test
on:
push:
branches: [ main ]
paths-ignore:
- 'samples/agent/adk/**'
pull_request:
paths-ignore:
- 'samples/agent/adk/**'
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install web lib deps
working-directory: ./renderers/lit
run: npm i
- name: Build web lib
working-directory: ./renderers/lit
run: npm run build
- name: Install renderer deps
working-directory: ./renderers/angular
run: npm i
- name: Build Angular renderer
working-directory: ./renderers/angular
run: npm run build
- name: Install top-level deps
working-directory: ./samples/client/angular
run: npm i
- name: Build contact sample
working-directory: ./samples/client/angular
run: npm run build contact
- name: Build restaurant sample
working-directory: ./samples/client/angular
run: npm run build restaurant
- name: Build Rizzchart sample
working-directory: ./samples/client/angular
run: npm run build rizzcharts
- name: Build Orchestrator
working-directory: ./samples/client/angular
run: npm run build orchestrator

View File

@@ -0,0 +1,62 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Build python samples
on:
push:
branches:
- main
paths:
- 'samples/agent/adk/**'
- 'a2a_agents/python/a2ui_extension/**'
pull_request:
paths:
- 'samples/agent/adk/**'
- 'a2a_agents/python/a2ui_extension/**'
jobs:
build:
name: Build samples
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install `uv` globally
run: |
python -m pip install --upgrade pip
pip install uv
- name: Build contact_lookup
working-directory: samples/agent/adk/contact_lookup
run: uv build .
- name: Build orchestrator
working-directory: samples/agent/adk/orchestrator
run: uv build .
- name: Build restaurant_finder
working-directory: samples/agent/adk/restaurant_finder
run: uv build .
- name: Build rizzcharts
working-directory: samples/agent/adk/rizzcharts
run: uv build .

View File

@@ -0,0 +1,50 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Lit renderer build and test
on:
push:
branches: [ main ]
paths:
- 'renderers/lit/**'
- '.github/workflows/web_build_and_test.yml'
pull_request:
paths:
- 'renderers/lit/**'
- '.github/workflows/web_build_and_test.yml'
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Lit renderer dependencies
working-directory: ./renderers/lit
run: npm i
- name: Build Lit renderer
working-directory: ./renderers/lit
run: npm run build
- name: Run Lit renderer tests
working-directory: ./renderers/lit
run: npm test

16
vendor/a2ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
node_modules
.DS_Store
.wireit
dist
.env
.idx
.vscode
__pycache__
*.pyc
.angular
# MkDocs build output
site/
# Python virtual environment
.venv/

49
vendor/a2ui/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,49 @@
# How to contribute to A2UI
We'd love to accept your patches and contributions to this project.
## Before you begin
### Sign our Contributor License Agreement
Contributions to this project must be accompanied by a
[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
You (or your employer) retain the copyright to your contribution; this simply
gives us permission to use and redistribute your contributions as part of the
project.
If you or your current employer have already signed the Google CLA (even if it
was for a different project), you probably don't need to do it again.
Visit <https://cla.developers.google.com/> to see your current agreements or to
sign a new one.
### Review our community guidelines
This project follows
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
## Contribution process
### Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
### Contributor Guide
You may follow these steps to contribute:
1. **Fork the official repository.** This will create a copy of the official repository in your own account.
2. **Sync the branches.** This will ensure that your copy of the repository is up-to-date with the latest changes from the official repository.
3. **Work on your forked repository's feature branch.** This is where you will make your changes to the code.
4. **Commit your updates on your forked repository's feature branch.** This will save your changes to your copy of the repository.
5. **Submit a pull request to the official repository's main branch.** This will request that your changes be merged into the official repository.
6. **Resolve any linting errors.** This will ensure that your changes are formatted correctly.
Here are some additional things to keep in mind during the process:
- **Test your changes.** Before you submit a pull request, make sure that your changes work as expected.
- **Be patient.** It may take some time for your pull request to be reviewed and merged.

203
vendor/a2ui/LICENSE vendored Normal file
View File

@@ -0,0 +1,203 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

162
vendor/a2ui/README.md vendored Normal file
View File

@@ -0,0 +1,162 @@
# A2UI: Agent-to-User Interface
A2UI is an open-source project, complete with a format
optimized for representing updateable agent-generated
UIs and an initial set of renderers, that allows agents
to generate or populate rich user interfaces.
<img src="docs/assets/a2ui_gallery_examples.png" alt="Gallery of A2UI components" height="400">
*A gallery of A2UI rendered cards, showing a variety of UI compositions that A2UI can achieve.*
## ⚠️ Status: Early Stage Public Preview
> **Note:** A2UI is currently in **v0.8 (Public Preview)**. The specification and
implementations are functional but are still evolving. We are opening the project to
foster collaboration, gather feedback, and solicit contributions (e.g., on client renderers).
Expect changes.
## Summary
Generative AI excels at creating text and code, but agents can struggle to
present rich, interactive interfaces to users, especially when those agents
are remote or running across trust boundaries.
**A2UI** is an open standard and set of libraries that allows agents to
"speak UI." Agents send a declarative JSON format describing the *intent* of
the UI. The client application then renders this using its own native
component library (Flutter, Angular, Lit, etc.).
This approach ensures that agent-generated UIs are
**safe like data, but expressive like code**.
## High-Level Philosophy
A2UI was designed to address the specific challenges of interoperable,
cross-platform, generative or template-based UI responses from agents.
The project's core philosophies:
* **Security first**: Running arbitrary code generated by an LLM may present a
security risk. A2UI is a declarative data format, not executable
code. Your client application maintains a "catalog" of trusted, pre-approved
UI components (e.g., Card, Button, TextField), and the agent can only request
to render components from that catalog.
* **LLM-friendly and incrementally updateable**: The UI is represented as a flat
list of components with ID references which is easy for LLMs to generate
incrementally, allowing for progressive rendering and a responsive user
experience. An agent can efficiently make incremental changes to the UI based
on new user requests as the conversation progresses.
* **Framework-agnostic and portable**: A2UI separates the UI structure from
the UI implementation. The agent sends a description of the component tree
and its associated data model. Your client application is responsible for
mapping these abstract descriptions to its native widgets—be it web components,
Flutter widgets, React components, SwiftUI views or something else entirely.
The same A2UI JSON payload from an agent can be rendered on multiple different
clients built on top of different frameworks.
* **Flexibility**: A2UI also features an open registry pattern that allows
developers to map server-side types to custom client implementations, from
native mobile widgets to React components. By registering a "Smart Wrapper,"
you can connect any existing UI component—including secure iframe containers
for legacy content—to A2UI's data binding and event system. Crucially, this
places security firmly in the developer's hands, enabling them to enforce
strict sandboxing policies and "trust ladders" directly within their custom
component logic rather than relying solely on the core system.
## Use Cases
Some of the use cases include:
* **Dynamic Data Collection:** An agent generates a bespoke form (date pickers,
sliders, inputs) based on the specific context of a conversation (e.g.,
booking a specialized reservation).
* **Remote Sub-Agents:** An orchestrator agent delegates a task to a
remote specialized agent (e.g., a travel booking agent) which returns a
UI payload to be rendered inside the main chat window.
* **Adaptive Workflows:** Enterprise agents that generate approval
dashboards or data visualizations on the fly based on the user's query.
## Architecture
The A2UI flow disconnects the generation of UI from the execution of UI:
1. **Generation:** An Agent (using Gemini or another LLM) generates or uses
a pre-generated `A2UI Response`, a JSON payload describing the composition
of UI components and their properties.
2. **Transport:** This message is sent to the client application
(via A2A, AG UI, etc.).
3. **Resolution:** The Client's **A2UI Renderer** parses the JSON.
4. **Rendering:** The Renderer maps the abstract components
(e.g., `type: 'text-field'`) to the concrete implementation in the client's codebase.
## Dependencies
A2UI is designed to be a lightweight format, but it fits into a larger ecosystem:
* **Transports:** Compatible with **A2A Protocol** and **AG UI**.
* **LLMs:** Can be generated by any model capable of generating JSON output.
* **Host Frameworks:** Requires a host application built in a supported framework
(currently: Web or Flutter).
## Getting Started
The best way to understand A2UI is to run the samples.
### Prerequisites
* Node.js (for web clients)
* Python (for agent samples)
* A valid [Gemini API Key](https://aistudio.google.com/) is required for the samples.
### Running the Restaurant Finder Demo
1. **Clone the repository:**
```bash
git clone https://github.com/google/A2UI.git
cd A2UI
```
2. **Set your API Key:**
```bash
export GEMINI_API_KEY="your_gemini_api_key"
```
3. **Run the Agent (Backend):**
```bash
cd samples/agent/adk/restaurant_finder
uv run .
```
4. **Run the Client (Frontend):**
Open a new terminal window:
```bash
cd samples/client/lit/shell
npm install
npm run dev
```
For Flutter developers, check out the [GenUI SDK](https://github.com/flutter/genui),
which uses A2UI under the hood.
CopilotKit has a public [A2UI Widget Builder](https://go.copilotkit.ai/A2UI-widget-builder)
to try out as well.
## Roadmap
We hope to work with the community on the following:
* **Spec Stabilization:** Moving towards a v1.0 specification.
* **More Renderers:** Adding official support for React, Jetpack Compose, iOS (SwiftUI), and more.
* **Additional Transports:** Support for REST and more.
* **Additional Agent Frameworks:** Genkit, LangGraph, and more.
## Contribute
A2UI is an **Apache 2.0** licensed project. We believe the future of UI is agentic,
and we want to work with you to help build it.
See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to get started.

184
vendor/a2ui/mkdocs.yaml vendored Normal file
View File

@@ -0,0 +1,184 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
site_name: A2UI
site_url: https://a2ui.org/
site_description: A2UI, a streaming protocol for Agent-Driven User Interfaces
site_author: Google
site_dir: site
extra:
analytics:
provider: google
property: G-YX9TPV8DCC
consent:
title: Cookie consent
description: >-
We use cookies to recognize repeated visits and preferences,
as well as to measure the effectiveness of our documentation and
whether users find the information they need. With your consent,
you're helping us to make our documentation better.
# Navigation
nav:
- Home: index.md
- Introduction & FAQ:
- What is A2UI?: introduction/what-is-a2ui.md
- Who is it For?: introduction/who-is-it-for.md
- How Can I Use It?: introduction/how-to-use.md
- Where is it Used?: introduction/where-is-it-used.md
- Agent UI Ecosystem: introduction/agent-ui-ecosystem.md
- Quickstart: quickstart.md
- A2UI Composer ⭐: composer.md
- Developer Guides:
- Client Setup: guides/client-setup.md
- Agent Development: guides/agent-development.md
- Custom Components: guides/custom-components.md
- Theming & Styling: guides/theming.md
- Core Concepts:
- Overview: concepts/overview.md
- Data Flow: concepts/data-flow.md
- Components & Structure: concepts/components.md
- Data Binding: concepts/data-binding.md
- Specifications:
- v0.8 (Stable):
- A2UI Specification: specification/v0.8-a2ui.md
- A2A Extension: specification/v0.8-a2a-extension.md
- v0.9 (Draft):
- A2UI Specification: specification/v0.9-a2ui.md
- Evolution Guide: specification/v0.9-evolution-guide.md
- Renderers (Clients): renderers.md
- Transports (Message Passing): transports.md
- Agents (Server-side): agents.md
- Community: community.md
- Roadmap: roadmap.md
- Reference:
- Component Reference: reference/components.md
- Message Reference: reference/messages.md
# Repository
repo_name: google/A2UI
repo_url: https://github.com/google/A2UI
# Copyright
copyright: Copyright Google 2025&nbsp;&nbsp;|&nbsp;&nbsp;<a href="//policies.google.com/terms">Terms</a>&nbsp;&nbsp;|&nbsp;&nbsp;<a href="//policies.google.com/privacy">Privacy</a>&nbsp;&nbsp;|&nbsp;&nbsp;<a href="#__consent">Manage cookies</a>
# Custom CSS
extra_css:
- stylesheets/custom.css
# Configuration
theme:
name: material
font:
text: Google Sans
code: Roboto Mono
logo: assets/A2UI_light.svg
favicon: assets/A2UI_dark.svg
icon:
repo: fontawesome/brands/github
# view: material/pencil-box-multiple
admonition:
note: fontawesome/solid/note-sticky
abstract: fontawesome/solid/book
info: fontawesome/solid/circle-info
tip: fontawesome/solid/bullhorn
success: fontawesome/solid/check
question: fontawesome/solid/circle-question
warning: fontawesome/solid/triangle-exclamation
failure: fontawesome/solid/bomb
danger: fontawesome/solid/skull
bug: fontawesome/solid/robot
example: fontawesome/solid/flask
quote: fontawesome/solid/quote-left
palette:
- scheme: default
primary: teal
accent: light blue
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: teal
accent: light blue
toggle:
icon: material/brightness-4
name: Switch to light mode
features:
- announce.dismiss
- content.action.view
- content.code.annotate
- content.code.copy
- content.code.select
- content.tabs.link
- navigation.footer
- navigation.indexes
- navigation.instant
- navigation.instant.progress
- navigation.path
- navigation.top
- navigation.tracking
- toc.follow
# Extensions
markdown_extensions:
- meta
- footnotes
- admonition
- attr_list
- md_in_html
- pymdownx.details
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets:
url_download: true
dedent_subsections: true
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:mermaid2.fence_mermaid
- pymdownx.tabbed:
alternate_style: true
slugify: !!python/object/apply:pymdownx.slugs.slugify
kwds:
case: lower
- pymdownx.tasklist:
custom_checkbox: true
- toc:
permalink: true
# Plugins
plugins:
- search
- macros
# - include-markdown
# - mermaid2
# - llmstxt:
# full_output: llms-full.txt
# sections:
# "Specification":
# - a2ui_protocol.md
# - redirects:
# redirect_maps:
# "index.md": "a2ui_protocol.md"

2
vendor/a2ui/renderers/angular/.npmrc vendored Normal file
View File

@@ -0,0 +1,2 @@
@a2ui:registry=https://us-npm.pkg.dev/oss-exit-gate-prod/a2ui--npm/
//us-npm.pkg.dev/oss-exit-gate-prod/a2ui--npm/:always-auth=true

View File

@@ -0,0 +1,9 @@
Angular implementation of A2UI.
Important: The sample code provided is for demonstration purposes and illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity.
All operational data received from an external agent—including its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide crafted data in its fields (e.g., name, skills.description) that, if used without sanitization to construct prompts for a Large Language Model (LLM), could expose your application to prompt injection attacks.
Similarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites.
Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users.

View File

@@ -0,0 +1,35 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"projects": {
"lib": {
"projectType": "library",
"root": ".",
"sourceRoot": "./src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular/build:ng-packagr",
"configurations": {
"production": {
"tsConfig": "./tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "./tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "./tsconfig.spec.json"
}
}
}
}
},
"cli": {
"analytics": false
}
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "./dist",
"lib": {
"entryFile": "src/public-api.ts"
},
"allowedNonPeerDependencies": ["markdown-it", "@a2ui/lit"]
}

14264
vendor/a2ui/renderers/angular/package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
{
"name": "@a2ui/angular",
"version": "0.8.1",
"scripts": {
"build": "ng build"
},
"dependencies": {
"@a2ui/lit": "file:../lit",
"markdown-it": "^14.1.0",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/platform-browser": "^21.0.0"
},
"devDependencies": {
"@angular/build": "^21.0.2",
"@angular/cli": "^21.0.2",
"@angular/compiler": "^21.0.0",
"@angular/compiler-cli": "^21.0.3",
"@angular/core": "^21.0.0",
"@types/express": "^5.0.1",
"@types/jasmine": "~5.1.0",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.17.19",
"@types/uuid": "^10.0.0",
"@vitest/browser": "^4.0.15",
"cypress": "^15.6.0",
"google-artifactregistry-auth": "^3.5.0",
"jasmine-core": "~5.9.0",
"jsdom": "^27.2.0",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
"karma-jasmine": "^5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",
"ng-packagr": "^21.0.0",
"playwright": "^1.56.1",
"prettier": "^3.6.2",
"sass": "^1.93.2",
"tslib": "^2.8.1",
"typescript": "~5.9.2",
"vitest": "^4.0.15"
},
"sideEffects": false,
"prettier": {
"printWidth": 100,
"singleQuote": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
}
}

View File

@@ -0,0 +1,50 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Primitives } from '@a2ui/lit/0.8';
@Component({
selector: 'a2ui-audio',
template: `
@let resolvedUrl = this.resolvedUrl();
@if (resolvedUrl) {
<section [class]="theme.components.AudioPlayer" [style]="theme.additionalStyles?.AudioPlayer">
<audio controls [src]="resolvedUrl"></audio>
</section>
}
`,
styles: `
:host {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
audio {
display: block;
width: 100%;
box-sizing: border-box;
}
`
})
export class Audio extends DynamicComponent {
readonly url = input.required<Primitives.StringValue | null>();
protected readonly resolvedUrl = computed(() => this.resolvePrimitive(this.url()));
}

View File

@@ -0,0 +1,56 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, input } from '@angular/core';
import { Types } from '@a2ui/lit/0.8';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Renderer } from '../rendering/renderer';
@Component({
selector: 'a2ui-button',
imports: [Renderer],
template: `
<button
[class]="theme.components.Button"
[style]="theme.additionalStyles?.Button"
(click)="handleClick()"
>
<ng-container
a2ui-renderer
[surfaceId]="surfaceId()!"
[component]="component().properties.child"
/>
</button>
`,
styles: `
:host {
display: block;
flex: var(--weight);
min-height: 0;
}
`,
})
export class Button extends DynamicComponent<Types.ButtonNode> {
readonly action = input.required<Types.Action | null>();
protected handleClick() {
const action = this.action();
if (action) {
super.sendAction(action);
}
}
}

View File

@@ -0,0 +1,57 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, ViewEncapsulation } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Renderer } from '../rendering/renderer';
import { Types } from '@a2ui/lit/0.8';
@Component({
selector: 'a2ui-card',
imports: [Renderer],
encapsulation: ViewEncapsulation.None,
styles: `
a2ui-card {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
a2ui-card > section {
height: 100%;
width: 100%;
min-height: 0;
overflow: auto;
}
a2ui-card > section > * {
height: 100%;
width: 100%;
}
`,
template: `
@let properties = component().properties;
@let children = properties.children || [properties.child];
<section [class]="theme.components.Card" [style]="theme.additionalStyles?.Card">
@for (child of children; track child) {
<ng-container a2ui-renderer [surfaceId]="surfaceId()!" [component]="child" />
}
</section>
`,
})
export class Card extends DynamicComponent<Types.CardNode> { }

View File

@@ -0,0 +1,73 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Primitives } from '@a2ui/lit/0.8';
@Component({
selector: 'a2ui-checkbox',
template: `
<section
[class]="theme.components.CheckBox.container"
[style]="theme.additionalStyles?.CheckBox"
>
<input
autocomplete="off"
type="checkbox"
[id]="inputId"
[checked]="inputChecked()"
[class]="theme.components.CheckBox.element"
(change)="handleChange($event)"
/>
<label [htmlFor]="inputId" [class]="theme.components.CheckBox.label">{{
resolvedLabel()
}}</label>
</section>
`,
styles: `
:host {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
input {
display: block;
width: 100%;
}
`,
})
export class Checkbox extends DynamicComponent {
readonly value = input.required<Primitives.BooleanValue | null>();
readonly label = input.required<Primitives.StringValue | null>();
protected inputChecked = computed(() => super.resolvePrimitive(this.value()) ?? false);
protected resolvedLabel = computed(() => super.resolvePrimitive(this.label()));
protected inputId = super.getUniqueId('a2ui-checkbox');
protected handleChange(event: Event) {
const path = this.value()?.path;
if (!(event.target instanceof HTMLInputElement) || !path) {
return;
}
this.processor.setData(this.component(), path, event.target.checked, this.surfaceId());
}
}

View File

@@ -0,0 +1,96 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input } from '@angular/core';
import { Types } from '@a2ui/lit/0.8';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Renderer } from '../rendering/renderer';
@Component({
selector: 'a2ui-column',
imports: [Renderer],
styles: `
:host {
display: flex;
flex: var(--weight);
}
section {
display: flex;
flex-direction: column;
min-width: 100%;
height: 100%;
box-sizing: border-box;
}
.align-start {
align-items: start;
}
.align-center {
align-items: center;
}
.align-end {
align-items: end;
}
.align-stretch {
align-items: stretch;
}
.distribute-start {
justify-content: start;
}
.distribute-center {
justify-content: center;
}
.distribute-end {
justify-content: end;
}
.distribute-spaceBetween {
justify-content: space-between;
}
.distribute-spaceAround {
justify-content: space-around;
}
.distribute-spaceEvenly {
justify-content: space-evenly;
}
`,
template: `
<section [class]="classes()" [style]="theme.additionalStyles?.Column">
@for (child of component().properties.children; track child) {
<ng-container a2ui-renderer [surfaceId]="surfaceId()!" [component]="child" />
}
</section>
`,
})
export class Column extends DynamicComponent<Types.ColumnNode> {
readonly alignment = input<Types.ResolvedColumn['alignment']>('stretch');
readonly distribution = input<Types.ResolvedColumn['distribution']>('start');
protected readonly classes = computed(() => ({
...this.theme.components.Column,
[`align-${this.alignment()}`]: true,
[`distribute-${this.distribution()}`]: true,
}));
}

View File

@@ -0,0 +1,127 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { computed, Component, input } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Primitives } from '@a2ui/lit/0.8';
@Component({
selector: 'a2ui-datetime-input',
template: `
<section [class]="theme.components.DateTimeInput.container">
<label [for]="inputId" [class]="theme.components.DateTimeInput.label">{{ label() }}</label>
<input
autocomplete="off"
[attr.type]="inputType()"
[id]="inputId"
[class]="theme.components.DateTimeInput.element"
[style]="theme.additionalStyles?.DateTimeInput"
[value]="inputValue()"
(input)="handleInput($event)"
/>
</section>
`,
styles: `
:host {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
input {
display: block;
width: 100%;
box-sizing: border-box;
}
`,
})
export class DatetimeInput extends DynamicComponent {
readonly value = input.required<Primitives.StringValue | null>();
readonly enableDate = input.required<boolean>();
readonly enableTime = input.required<boolean>();
protected readonly inputId = super.getUniqueId('a2ui-datetime-input');
protected inputType = computed(() => {
const enableDate = this.enableDate();
const enableTime = this.enableTime();
if (enableDate && enableTime) {
return 'datetime-local';
} else if (enableDate) {
return 'date';
} else if (enableTime) {
return 'time';
}
return 'datetime-local';
});
protected label = computed(() => {
// TODO: this should likely be passed from the model.
const inputType = this.inputType();
if (inputType === 'date') {
return 'Date';
} else if (inputType === 'time') {
return 'Time';
}
return 'Date & Time';
});
protected inputValue = computed(() => {
const inputType = this.inputType();
const parsed = super.resolvePrimitive(this.value()) || '';
const date = parsed ? new Date(parsed) : null;
if (!date || isNaN(date.getTime())) {
return '';
}
const year = this.padNumber(date.getFullYear());
const month = this.padNumber(date.getMonth());
const day = this.padNumber(date.getDate());
const hours = this.padNumber(date.getHours());
const minutes = this.padNumber(date.getMinutes());
// Browsers are picky with what format they allow for the `value` attribute of date/time inputs.
// We need to parse it out of the provided value. Note that we don't use `toISOString`,
// because the resulting value is relative to UTC.
if (inputType === 'date') {
return `${year}-${month}-${day}`;
} else if (inputType === 'time') {
return `${hours}:${minutes}`;
}
return `${year}-${month}-${day}T${hours}:${minutes}`;
});
protected handleInput(event: Event) {
const path = this.value()?.path;
if (!(event.target instanceof HTMLInputElement) || !path) {
return;
}
this.processor.setData(this.component(), path, event.target.value, this.surfaceId());
}
private padNumber(value: number) {
return value.toString().padStart(2, '0');
}
}

View File

@@ -0,0 +1,185 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { inputBinding } from '@angular/core';
import { Types } from '@a2ui/lit/0.8';
import { Catalog } from '../rendering/catalog';
import { Row } from './row';
import { Column } from './column';
import { Text } from './text';
export const DEFAULT_CATALOG: Catalog = {
Row: {
type: () => Row,
bindings: (node) => {
const properties = (node as Types.RowNode).properties;
return [
inputBinding('alignment', () => properties.alignment ?? 'stretch'),
inputBinding('distribution', () => properties.distribution ?? 'start'),
];
},
},
Column: {
type: () => Column,
bindings: (node) => {
const properties = (node as Types.ColumnNode).properties;
return [
inputBinding('alignment', () => properties.alignment ?? 'stretch'),
inputBinding('distribution', () => properties.distribution ?? 'start'),
];
},
},
List: {
type: () => import('./list').then((r) => r.List),
bindings: (node) => {
const properties = (node as Types.ListNode).properties;
return [inputBinding('direction', () => properties.direction ?? 'vertical')];
},
},
Card: () => import('./card').then((r) => r.Card),
Image: {
type: () => import('./image').then((r) => r.Image),
bindings: (node) => {
const properties = (node as Types.ImageNode).properties;
return [
inputBinding('url', () => properties.url),
inputBinding('usageHint', () => properties.usageHint),
];
},
},
Icon: {
type: () => import('./icon').then((r) => r.Icon),
bindings: (node) => {
const properties = (node as Types.IconNode).properties;
return [inputBinding('name', () => properties.name)];
},
},
Video: {
type: () => import('./video').then((r) => r.Video),
bindings: (node) => {
const properties = (node as Types.VideoNode).properties;
return [inputBinding('url', () => properties.url)];
},
},
AudioPlayer: {
type: () => import('./audio').then((r) => r.Audio),
bindings: (node) => {
const properties = (node as Types.AudioPlayerNode).properties;
return [inputBinding('url', () => properties.url)];
},
},
Text: {
type: () => Text,
bindings: (node) => {
const properties = (node as Types.TextNode).properties;
return [
inputBinding('text', () => properties.text),
inputBinding('usageHint', () => properties.usageHint || null),
];
},
},
Button: {
type: () => import('./button').then((r) => r.Button),
bindings: (node) => {
const properties = (node as Types.ButtonNode).properties;
return [inputBinding('action', () => properties.action)];
},
},
Divider: () => import('./divider').then((r) => r.Divider),
MultipleChoice: {
type: () => import('./multiple-choice').then((r) => r.MultipleChoice),
bindings: (node) => {
const properties = (node as Types.MultipleChoiceNode).properties;
return [
inputBinding('options', () => properties.options || []),
inputBinding('value', () => properties.selections),
inputBinding('description', () => 'Select an item'), // TODO: this should be defined in the properties
];
},
},
TextField: {
type: () => import('./text-field').then((r) => r.TextField),
bindings: (node) => {
const properties = (node as Types.TextFieldNode).properties;
return [
inputBinding('text', () => properties.text ?? null),
inputBinding('label', () => properties.label),
inputBinding('inputType', () => properties.type),
];
},
},
DateTimeInput: {
type: () => import('./datetime-input').then((r) => r.DatetimeInput),
bindings: (node) => {
const properties = (node as Types.DateTimeInputNode).properties;
return [
inputBinding('enableDate', () => properties.enableDate),
inputBinding('enableTime', () => properties.enableTime),
inputBinding('value', () => properties.value),
];
},
},
CheckBox: {
type: () => import('./checkbox').then((r) => r.Checkbox),
bindings: (node) => {
const properties = (node as Types.CheckboxNode).properties;
return [
inputBinding('label', () => properties.label),
inputBinding('value', () => properties.value),
];
},
},
Slider: {
type: () => import('./slider').then((r) => r.Slider),
bindings: (node) => {
const properties = (node as Types.SliderNode).properties;
return [
inputBinding('value', () => properties.value),
inputBinding('minValue', () => properties.minValue),
inputBinding('maxValue', () => properties.maxValue),
inputBinding('label', () => ''), // TODO: this should be defined in the properties
];
},
},
Tabs: {
type: () => import('./tabs').then((r) => r.Tabs),
bindings: (node) => {
const properties = (node as Types.TabsNode).properties;
return [inputBinding('tabs', () => properties.tabItems)];
},
},
Modal: {
type: () => import('./modal').then((r) => r.Modal),
bindings: () => [],
},
};

View File

@@ -0,0 +1,37 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
@Component({
selector: 'a2ui-divider',
template: '<hr [class]="theme.components.Divider" [style]="theme.additionalStyles?.Divider"/>',
styles: `
:host {
display: block;
min-height: 0;
overflow: auto;
}
hr {
height: 1px;
background: #ccc;
border: none;
}
`,
})
export class Divider extends DynamicComponent {}

View File

@@ -0,0 +1,44 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Primitives } from '@a2ui/lit/0.8';
@Component({
selector: 'a2ui-icon',
styles: `
:host {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
`,
template: `
@let resolvedName = this.resolvedName();
@if (resolvedName) {
<section [class]="theme.components.Icon" [style]="theme.additionalStyles?.Icon">
<span class="g-icon">{{ resolvedName }}</span>
</section>
}
`,
})
export class Icon extends DynamicComponent {
readonly name = input.required<Primitives.StringValue | null>();
protected readonly resolvedName = computed(() => this.resolvePrimitive(this.name()));
}

View File

@@ -0,0 +1,62 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input } from '@angular/core';
import { Primitives, Styles, Types } from '@a2ui/lit/0.8';
import { DynamicComponent } from '../rendering/dynamic-component';
@Component({
selector: 'a2ui-image',
styles: `
:host {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
img {
display: block;
width: 100%;
height: 100%;
box-sizing: border-box;
}
`,
template: `
@let resolvedUrl = this.resolvedUrl();
@if (resolvedUrl) {
<section [class]="classes()" [style]="theme.additionalStyles?.Image">
<img [src]="resolvedUrl" />
</section>
}
`,
})
export class Image extends DynamicComponent {
readonly url = input.required<Primitives.StringValue | null>();
readonly usageHint = input.required<Types.ResolvedImage['usageHint'] | null>();
protected readonly resolvedUrl = computed(() => this.resolvePrimitive(this.url()));
protected classes = computed(() => {
const usageHint = this.usageHint();
return Styles.merge(
this.theme.components.Image.all,
usageHint ? this.theme.components.Image[usageHint] : {},
);
});
}

View File

@@ -0,0 +1,63 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, input } from '@angular/core';
import { Types } from '@a2ui/lit/0.8';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Renderer } from '../rendering/renderer';
@Component({
selector: 'a2ui-list',
imports: [Renderer],
host: {
'[attr.direction]': 'direction()',
},
styles: `
:host {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
:host([direction='vertical']) section {
display: grid;
}
:host([direction='horizontal']) section {
display: flex;
max-width: 100%;
overflow-x: scroll;
overflow-y: hidden;
scrollbar-width: none;
> ::slotted(*) {
flex: 1 0 fit-content;
max-width: min(80%, 400px);
}
}
`,
template: `
<section [class]="theme.components.List" [style]="theme.additionalStyles?.List">
@for (child of component().properties.children; track child) {
<ng-container a2ui-renderer [surfaceId]="surfaceId()!" [component]="child" />
}
</section>
`,
})
export class List extends DynamicComponent<Types.ListNode> {
readonly direction = input<'vertical' | 'horizontal'>('vertical');
}

View File

@@ -0,0 +1,113 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, signal, viewChild, ElementRef, effect } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Types } from '@a2ui/lit/0.8';
import { Renderer } from '../rendering';
@Component({
selector: 'a2ui-modal',
imports: [Renderer],
template: `
@if (showDialog()) {
<dialog #dialog [class]="theme.components.Modal.backdrop" (click)="handleDialogClick($event)">
<section [class]="theme.components.Modal.element" [style]="theme.additionalStyles?.Modal">
<div class="controls">
<button (click)="closeDialog()">
<span class="g-icon">close</span>
</button>
</div>
<ng-container
a2ui-renderer
[surfaceId]="surfaceId()!"
[component]="component().properties.contentChild"
/>
</section>
</dialog>
} @else {
<section (click)="showDialog.set(true)">
<ng-container
a2ui-renderer
[surfaceId]="surfaceId()!"
[component]="component().properties.entryPointChild"
/>
</section>
}
`,
styles: `
dialog {
padding: 0;
border: none;
background: none;
& section {
& .controls {
display: flex;
justify-content: end;
margin-bottom: 4px;
& button {
padding: 0;
background: none;
width: 20px;
height: 20px;
pointer: cursor;
border: none;
cursor: pointer;
}
}
}
}
`,
})
export class Modal extends DynamicComponent<Types.ModalNode> {
protected readonly showDialog = signal(false);
protected readonly dialog = viewChild<ElementRef<HTMLDialogElement>>('dialog');
constructor() {
super();
effect(() => {
const dialog = this.dialog();
if (dialog && !dialog.nativeElement.open) {
dialog.nativeElement.showModal();
}
});
}
protected handleDialogClick(event: MouseEvent) {
if (event.target instanceof HTMLDialogElement) {
this.closeDialog();
}
}
protected closeDialog() {
const dialog = this.dialog();
if (!dialog) {
return;
}
if (!dialog.nativeElement.open) {
dialog.nativeElement.close();
}
this.showDialog.set(false);
}
}

View File

@@ -0,0 +1,77 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Primitives } from '@a2ui/lit/0.8';
@Component({
selector: 'a2ui-multiple-choice',
template: `
<section [class]="theme.components.MultipleChoice.container">
<label [class]="theme.components.MultipleChoice.label" [for]="selectId">{{
description()
}}</label>
<select
(change)="handleChange($event)"
[id]="selectId"
[value]="selectValue()"
[class]="theme.components.MultipleChoice.element"
[style]="theme.additionalStyles?.MultipleChoice"
>
@for (option of options(); track option.value) {
<option [value]="option.value">{{ resolvePrimitive(option.label) }}</option>
}
</select>
</section>
`,
styles: `
:host {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
select {
width: 100%;
box-sizing: border-box;
}
`,
})
export class MultipleChoice extends DynamicComponent {
readonly options = input.required<{ label: Primitives.StringValue; value: string }[]>();
readonly value = input.required<Primitives.StringValue | null>();
readonly description = input.required<string>();
protected readonly selectId = super.getUniqueId('a2ui-multiple-choice');
protected selectValue = computed(() => super.resolvePrimitive(this.value()));
protected handleChange(event: Event) {
const path = this.value()?.path;
if (!(event.target instanceof HTMLSelectElement) || !event.target.value || !path) {
return;
}
this.processor.setData(
this.component(),
this.processor.resolvePath(path, this.component().dataContextPath),
event.target.value,
);
}
}

View File

@@ -0,0 +1,100 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Renderer } from '../rendering/renderer';
import { Types } from '@a2ui/lit/0.8';
@Component({
selector: 'a2ui-row',
imports: [Renderer],
host: {
'[attr.alignment]': 'alignment()',
'[attr.distribution]': 'distribution()',
},
styles: `
:host {
display: flex;
flex: var(--weight);
}
section {
display: flex;
flex-direction: row;
width: 100%;
min-height: 100%;
box-sizing: border-box;
}
.align-start {
align-items: start;
}
.align-center {
align-items: center;
}
.align-end {
align-items: end;
}
.align-stretch {
align-items: stretch;
}
.distribute-start {
justify-content: start;
}
.distribute-center {
justify-content: center;
}
.distribute-end {
justify-content: end;
}
.distribute-spaceBetween {
justify-content: space-between;
}
.distribute-spaceAround {
justify-content: space-around;
}
.distribute-spaceEvenly {
justify-content: space-evenly;
}
`,
template: `
<section [class]="classes()" [style]="theme.additionalStyles?.Row">
@for (child of component().properties.children; track child) {
<ng-container a2ui-renderer [surfaceId]="surfaceId()!" [component]="child" />
}
</section>
`,
})
export class Row extends DynamicComponent<Types.RowNode> {
readonly alignment = input<Types.ResolvedRow['alignment']>('stretch');
readonly distribution = input<Types.ResolvedRow['distribution']>('start');
protected readonly classes = computed(() => ({
...this.theme.components.Row,
[`align-${this.alignment()}`]: true,
[`distribute-${this.distribution()}`]: true,
}));
}

View File

@@ -0,0 +1,73 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input } from '@angular/core';
import { Primitives } from '@a2ui/lit/0.8';
import { DynamicComponent } from '../rendering/dynamic-component';
@Component({
selector: '[a2ui-slider]',
template: `
<section [class]="theme.components.Slider.container">
<label [class]="theme.components.Slider.label" [for]="inputId">
{{ label() }}
</label>
<input
autocomplete="off"
type="range"
[value]="resolvedValue()"
[min]="minValue()"
[max]="maxValue()"
[id]="inputId"
(input)="handleInput($event)"
[class]="theme.components.Slider.element"
[style]="theme.additionalStyles?.Slider"
/>
</section>
`,
styles: `
:host {
display: block;
flex: var(--weight);
}
input {
display: block;
width: 100%;
box-sizing: border-box;
}
`,
})
export class Slider extends DynamicComponent {
readonly value = input.required<Primitives.NumberValue | null>();
readonly label = input('');
readonly minValue = input.required<number | undefined>();
readonly maxValue = input.required<number | undefined>();
protected readonly inputId = super.getUniqueId('a2ui-slider');
protected resolvedValue = computed(() => super.resolvePrimitive(this.value()) ?? 0);
protected handleInput(event: Event) {
const path = this.value()?.path;
if (!(event.target instanceof HTMLInputElement) || !path) {
return;
}
this.processor.setData(this.component(), path, event.target.valueAsNumber, this.surfaceId());
}
}

View File

@@ -0,0 +1,99 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input } from '@angular/core';
import { Types } from '@a2ui/lit/0.8';
import { Renderer } from '../rendering/renderer';
@Component({
selector: 'a2ui-surface',
imports: [Renderer],
template: `
@let surfaceId = this.surfaceId();
@let surface = this.surface();
@if (surfaceId && surface) {
<ng-container a2ui-renderer [surfaceId]="surfaceId" [component]="surface.componentTree!" />
}
`,
styles: `
:host {
display: flex;
min-height: 0;
max-height: 100%;
flex-direction: column;
gap: 16px;
}
`,
host: {
'[style]': 'styles()',
},
})
export class Surface {
readonly surfaceId = input.required<Types.SurfaceID | null>();
readonly surface = input.required<Types.Surface | null>();
protected readonly styles = computed(() => {
const surface = this.surface();
const styles: Record<string, string> = {};
if (surface?.styles) {
for (const [key, value] of Object.entries(surface.styles)) {
switch (key) {
// Here we generate a palette from the singular primary color received
// from the surface data. We will want the values to range from
// 0 <= x <= 100, where 0 = back, 100 = white, and 50 = the primary
// color itself. As such we use a color-mix to create the intermediate
// values.
//
// Note: since we use half the range for black to the primary color,
// and half the range for primary color to white the mixed values have
// to go up double the amount, i.e., a range from black to primary
// color needs to fit in 0 -> 50 rather than 0 -> 100.
case 'primaryColor': {
styles['--p-100'] = '#ffffff';
styles['--p-99'] = `color-mix(in srgb, ${value} 2%, white 98%)`;
styles['--p-98'] = `color-mix(in srgb, ${value} 4%, white 96%)`;
styles['--p-95'] = `color-mix(in srgb, ${value} 10%, white 90%)`;
styles['--p-90'] = `color-mix(in srgb, ${value} 20%, white 80%)`;
styles['--p-80'] = `color-mix(in srgb, ${value} 40%, white 60%)`;
styles['--p-70'] = `color-mix(in srgb, ${value} 60%, white 40%)`;
styles['--p-60'] = `color-mix(in srgb, ${value} 80%, white 20%)`;
styles['--p-50'] = value;
styles['--p-40'] = `color-mix(in srgb, ${value} 80%, black 20%)`;
styles['--p-35'] = `color-mix(in srgb, ${value} 70%, black 30%)`;
styles['--p-30'] = `color-mix(in srgb, ${value} 60%, black 40%)`;
styles['--p-25'] = `color-mix(in srgb, ${value} 50%, black 50%)`;
styles['--p-20'] = `color-mix(in srgb, ${value} 40%, black 60%)`;
styles['--p-15'] = `color-mix(in srgb, ${value} 30%, black 70%)`;
styles['--p-10'] = `color-mix(in srgb, ${value} 20%, black 80%)`;
styles['--p-5'] = `color-mix(in srgb, ${value} 10%, black 90%)`;
styles['--0'] = '#00000';
break;
}
case 'font': {
styles['--font-family'] = value;
styles['--font-family-flex'] = value;
break;
}
}
}
}
return styles;
});
}

View File

@@ -0,0 +1,72 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input, signal } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Renderer } from '../rendering/renderer';
import { Styles, Types } from '@a2ui/lit/0.8';
@Component({
selector: 'a2ui-tabs',
imports: [Renderer],
template: `
@let tabs = this.tabs();
@let selectedIndex = this.selectedIndex();
<section [class]="theme.components.Tabs.container" [style]="theme.additionalStyles?.Tabs">
<div [class]="theme.components.Tabs.element">
@for (tab of tabs; track tab) {
<button
(click)="this.selectedIndex.set($index)"
[disabled]="selectedIndex === $index"
[class]="buttonClasses()[selectedIndex]"
>
{{ resolvePrimitive(tab.title) }}
</button>
}
</div>
<ng-container
a2ui-renderer
[surfaceId]="surfaceId()!"
[component]="tabs[selectedIndex].child"
/>
</section>
`,
styles: `
:host {
display: block;
flex: var(--weight);
}
`,
})
export class Tabs extends DynamicComponent {
protected selectedIndex = signal(0);
readonly tabs = input.required<Types.ResolvedTabItem[]>();
protected readonly buttonClasses = computed(() => {
const selectedIndex = this.selectedIndex();
return this.tabs().map((_, index) => {
return index === selectedIndex
? Styles.merge(
this.theme.components.Tabs.controls.all,
this.theme.components.Tabs.controls.selected,
)
: this.theme.components.Tabs.controls.all;
});
});
}

View File

@@ -0,0 +1,86 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { computed, Component, input } from '@angular/core';
import { Primitives, Types } from '@a2ui/lit/0.8';
import { DynamicComponent } from '../rendering/dynamic-component';
@Component({
selector: 'a2ui-text-field',
styles: `
:host {
display: flex;
flex: var(--weight);
}
section,
input,
label {
box-sizing: border-box;
}
input {
display: block;
width: 100%;
}
label {
display: block;
margin-bottom: 4px;
}
`,
template: `
@let resolvedLabel = this.resolvedLabel();
<section [class]="theme.components.TextField.container">
@if (resolvedLabel) {
<label [for]="inputId" [class]="theme.components.TextField.label">{{
resolvedLabel
}}</label>
}
<input
autocomplete="off"
[class]="theme.components.TextField.element"
[style]="theme.additionalStyles?.TextField"
(input)="handleInput($event)"
[id]="inputId"
[value]="inputValue()"
placeholder="Please enter a value"
[type]="inputType() === 'number' ? 'number' : 'text'"
/>
</section>
`,
})
export class TextField extends DynamicComponent {
readonly text = input.required<Primitives.StringValue | null>();
readonly label = input.required<Primitives.StringValue | null>();
readonly inputType = input.required<Types.ResolvedTextField['type'] | null>();
protected inputValue = computed(() => super.resolvePrimitive(this.text()) || '');
protected resolvedLabel = computed(() => super.resolvePrimitive(this.label()));
protected inputId = super.getUniqueId('a2ui-input');
protected handleInput(event: Event) {
const path = this.text()?.path;
if (!(event.target instanceof HTMLInputElement) || !path) {
return;
}
this.processor.setData(this.component(), path, event.target.value, this.surfaceId());
}
}

View File

@@ -0,0 +1,137 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, inject, input, ViewEncapsulation } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Primitives, Styles, Types } from '@a2ui/lit/0.8';
import { MarkdownRenderer } from '../data/markdown';
interface HintedStyles {
h1: Record<string, string>;
h2: Record<string, string>;
h3: Record<string, string>;
h4: Record<string, string>;
h5: Record<string, string>;
body: Record<string, string>;
caption: Record<string, string>;
}
@Component({
selector: 'a2ui-text',
template: `
<section
[class]="classes()"
[style]="additionalStyles()"
[innerHTML]="resolvedText()"
></section>
`,
encapsulation: ViewEncapsulation.None,
styles: `
a2ui-text {
display: block;
flex: var(--weight);
}
a2ui-text h1,
a2ui-text h2,
a2ui-text h3,
a2ui-text h4,
a2ui-text h5 {
line-height: inherit;
font: inherit;
}
`,
})
export class Text extends DynamicComponent {
private markdownRenderer = inject(MarkdownRenderer);
readonly text = input.required<Primitives.StringValue | null>();
readonly usageHint = input.required<Types.ResolvedText['usageHint'] | null>();
protected resolvedText = computed(() => {
const usageHint = this.usageHint();
let value = super.resolvePrimitive(this.text());
if (value == null) {
return '(empty)';
}
switch (usageHint) {
case 'h1':
value = `# ${value}`;
break;
case 'h2':
value = `## ${value}`;
break;
case 'h3':
value = `### ${value}`;
break;
case 'h4':
value = `#### ${value}`;
break;
case 'h5':
value = `##### ${value}`;
break;
case 'caption':
value = `*${value}*`;
break;
default:
value = String(value);
break;
}
return this.markdownRenderer.render(
value,
Styles.appendToAll(this.theme.markdown, ['ol', 'ul', 'li'], {}),
);
});
protected classes = computed(() => {
const usageHint = this.usageHint();
return Styles.merge(
this.theme.components.Text.all,
usageHint ? this.theme.components.Text[usageHint] : {},
);
});
protected additionalStyles = computed(() => {
const usageHint = this.usageHint();
const styles = this.theme.additionalStyles?.Text;
if (!styles) {
return null;
}
let additionalStyles: Record<string, string> = {};
if (this.areHintedStyles(styles)) {
additionalStyles = styles[usageHint ?? 'body'];
} else {
additionalStyles = styles;
}
return additionalStyles;
});
private areHintedStyles(styles: unknown): styles is HintedStyles {
if (typeof styles !== 'object' || !styles || Array.isArray(styles)) {
return false;
}
const expected = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'caption', 'body'];
return expected.every((v) => v in styles);
}
}

View File

@@ -0,0 +1,50 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Component, computed, input } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
import { Primitives } from '@a2ui/lit/0.8';
@Component({
selector: 'a2ui-video',
template: `
@let resolvedUrl = this.resolvedUrl();
@if (resolvedUrl) {
<section [class]="theme.components.Video" [style]="theme.additionalStyles?.Video">
<video controls [src]="resolvedUrl"></video>
</section>
}
`,
styles: `
:host {
display: block;
flex: var(--weight);
min-height: 0;
overflow: auto;
}
video {
display: block;
width: 100%;
box-sizing: border-box;
}
`,
})
export class Video extends DynamicComponent {
readonly url = input.required<Primitives.StringValue | null>();
protected readonly resolvedUrl = computed(() => this.resolvePrimitive(this.url()));
}

View File

@@ -0,0 +1,25 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
import { Catalog, Theme } from './rendering';
export function provideA2UI(config: { catalog: Catalog; theme: Theme }): EnvironmentProviders {
return makeEnvironmentProviders([
{ provide: Catalog, useValue: config.catalog },
{ provide: Theme, useValue: config.theme },
]);
}

View File

@@ -0,0 +1,18 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export * from './processor';
export * from './types';

View File

@@ -0,0 +1,114 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { inject, Injectable, SecurityContext } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import MarkdownIt from 'markdown-it';
@Injectable({ providedIn: 'root' })
export class MarkdownRenderer {
private originalClassMap = new Map<string, any>();
private sanitizer = inject(DomSanitizer);
private markdownIt = MarkdownIt({
highlight: (str, lang) => {
if (lang === 'html') {
const iframe = document.createElement('iframe');
iframe.classList.add('html-view');
iframe.srcdoc = str;
iframe.sandbox = '';
return iframe.innerHTML;
}
return str;
},
});
render(value: string, tagClassMap?: Record<string, string[]>) {
if (tagClassMap) {
this.applyTagClassMap(tagClassMap);
}
const htmlString = this.markdownIt.render(value);
this.unapplyTagClassMap();
return this.sanitizer.sanitize(SecurityContext.HTML, htmlString);
}
private applyTagClassMap(tagClassMap: Record<string, string[]>) {
Object.entries(tagClassMap).forEach(([tag, classes]) => {
let tokenName;
switch (tag) {
case 'p':
tokenName = 'paragraph';
break;
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
tokenName = 'heading';
break;
case 'ul':
tokenName = 'bullet_list';
break;
case 'ol':
tokenName = 'ordered_list';
break;
case 'li':
tokenName = 'list_item';
break;
case 'a':
tokenName = 'link';
break;
case 'strong':
tokenName = 'strong';
break;
case 'em':
tokenName = 'em';
break;
}
if (!tokenName) {
return;
}
const key = `${tokenName}_open`;
const original = this.markdownIt.renderer.rules[key];
this.originalClassMap.set(key, original);
this.markdownIt.renderer.rules[key] = (tokens, idx, options, env, self) => {
const token = tokens[idx];
for (const clazz of classes) {
token.attrJoin('class', clazz);
}
if (original) {
return original.call(this, tokens, idx, options, env, self);
} else {
return self.renderToken(tokens, idx, options);
}
};
});
}
private unapplyTagClassMap() {
for (const [key, original] of this.originalClassMap) {
this.markdownIt.renderer.rules[key] = original;
}
this.originalClassMap.clear();
}
}

View File

@@ -0,0 +1,47 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Data, Types } from '@a2ui/lit/0.8';
import { Injectable } from '@angular/core';
import { firstValueFrom, Subject } from 'rxjs';
export interface DispatchedEvent {
message: Types.A2UIClientEventMessage;
completion: Subject<Types.ServerToClientMessage[]>;
}
@Injectable({ providedIn: 'root' })
export class MessageProcessor extends Data.A2uiMessageProcessor {
readonly events = new Subject<DispatchedEvent>();
override setData(
node: Types.AnyComponentNode,
relativePath: string,
value: Types.DataValue,
surfaceId?: Types.SurfaceID | null,
) {
// Override setData to convert from optional inputs (which can be null)
// to undefined so that this correctly falls back to the default value for
// surfaceId.
return super.setData(node, relativePath, value, surfaceId ?? undefined);
}
dispatch(message: Types.A2UIClientEventMessage): Promise<Types.ServerToClientMessage[]> {
const completion = new Subject<Types.ServerToClientMessage[]>();
this.events.next({ message, completion });
return firstValueFrom(completion);
}
}

View File

@@ -0,0 +1,29 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Types } from '@a2ui/lit/0.8';
export interface A2TextPayload {
kind: 'text';
text: string;
}
export interface A2DataPayload {
kind: 'data';
data: Types.ServerToClientMessage;
}
export type A2AServerPayload = Array<A2DataPayload | A2TextPayload> | { error: string };

View File

@@ -0,0 +1,36 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Binding, InjectionToken, Type } from '@angular/core';
import { DynamicComponent } from './dynamic-component';
import { Types } from '@a2ui/lit/0.8';
export type CatalogLoader = () =>
| Promise<Type<DynamicComponent<any>>>
| Type<DynamicComponent<any>>;
export type CatalogEntry<T extends Types.AnyComponentNode> =
| CatalogLoader
| {
type: CatalogLoader;
bindings: (data: T) => Binding[];
};
export interface Catalog {
[key: string]: CatalogEntry<Types.AnyComponentNode>;
}
export const Catalog = new InjectionToken<Catalog>('Catalog');

View File

@@ -0,0 +1,100 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Types, Primitives } from '@a2ui/lit/0.8';
import { Directive, inject, input } from '@angular/core';
import { MessageProcessor } from '../data';
import { Theme } from './theming';
let idCounter = 0;
@Directive({
host: {
'[style.--weight]': 'weight()',
},
})
export abstract class DynamicComponent<T extends Types.AnyComponentNode = Types.AnyComponentNode> {
protected readonly processor = inject(MessageProcessor);
protected readonly theme = inject(Theme);
readonly surfaceId = input.required<Types.SurfaceID | null>();
readonly component = input.required<T>();
readonly weight = input.required<string | number>();
protected sendAction(action: Types.Action): Promise<Types.ServerToClientMessage[]> {
const component = this.component();
const surfaceId = this.surfaceId() ?? undefined;
const context: Record<string, unknown> = {};
if (action.context) {
for (const item of action.context) {
if (item.value.literalBoolean) {
context[item.key] = item.value.literalBoolean;
} else if (item.value.literalNumber) {
context[item.key] = item.value.literalNumber;
} else if (item.value.literalString) {
context[item.key] = item.value.literalString;
} else if (item.value.path) {
const path = this.processor.resolvePath(item.value.path, component.dataContextPath);
const value = this.processor.getData(component, path, surfaceId);
context[item.key] = value;
}
}
}
const message: Types.A2UIClientEventMessage = {
userAction: {
name: action.name,
sourceComponentId: component.id,
surfaceId: surfaceId!,
timestamp: new Date().toISOString(),
context,
},
};
return this.processor.dispatch(message);
}
protected resolvePrimitive(value: Primitives.StringValue | null): string | null;
protected resolvePrimitive(value: Primitives.BooleanValue | null): boolean | null;
protected resolvePrimitive(value: Primitives.NumberValue | null): number | null;
protected resolvePrimitive(
value: Primitives.StringValue | Primitives.BooleanValue | Primitives.NumberValue | null,
) {
const component = this.component();
const surfaceId = this.surfaceId();
if (!value || typeof value !== 'object') {
return null;
} else if (value.literal != null) {
return value.literal;
} else if (value.path) {
return this.processor.getData(component, value.path, surfaceId ?? undefined);
} else if ('literalString' in value) {
return value.literalString;
} else if ('literalNumber' in value) {
return value.literalNumber;
} else if ('literalBoolean' in value) {
return value.literalBoolean;
}
return null;
}
protected getUniqueId(prefix: string) {
return `${prefix}-${idCounter++}`;
}
}

View File

@@ -0,0 +1,20 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export * from './catalog';
export * from './dynamic-component';
export * from './renderer';
export * from './theming';

View File

@@ -0,0 +1,109 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
Binding,
ComponentRef,
Directive,
DOCUMENT,
effect,
inject,
input,
inputBinding,
OnDestroy,
PLATFORM_ID,
Type,
untracked,
ViewContainerRef,
} from '@angular/core';
import { Types, Styles } from '@a2ui/lit/0.8';
import { Catalog } from './catalog';
import { isPlatformBrowser } from '@angular/common';
@Directive({
selector: 'ng-container[a2ui-renderer]',
})
export class Renderer implements OnDestroy {
private viewContainerRef = inject(ViewContainerRef);
private catalog = inject(Catalog);
private static hasInsertedStyles = false;
private currentRef: ComponentRef<unknown> | null = null;
private isDestroyed = false;
readonly surfaceId = input.required<Types.SurfaceID>();
readonly component = input.required<Types.AnyComponentNode>();
constructor() {
effect(() => {
const surfaceId = this.surfaceId();
const component = this.component();
untracked(() => this.render(surfaceId, component));
});
const platformId = inject(PLATFORM_ID);
const document = inject(DOCUMENT);
if (!Renderer.hasInsertedStyles && isPlatformBrowser(platformId)) {
const styles = document.createElement('style');
styles.textContent = Styles.structuralStyles;
document.head.appendChild(styles);
Renderer.hasInsertedStyles = true;
}
}
ngOnDestroy(): void {
this.isDestroyed = true;
this.clear();
}
private async render(surfaceId: Types.SurfaceID, component: Types.AnyComponentNode) {
const config = this.catalog[component.type];
let newComponent: Type<unknown> | null = null;
let componentBindings: Binding[] | null = null;
if (typeof config === 'function') {
newComponent = await config();
} else if (typeof config === 'object') {
newComponent = await config.type();
componentBindings = config.bindings(component as any);
}
this.clear();
if (newComponent && !this.isDestroyed) {
const bindings = [
inputBinding('surfaceId', () => surfaceId),
inputBinding('component', () => component),
inputBinding('weight', () => component.weight ?? 'initial'),
];
if (componentBindings) {
bindings.push(...componentBindings);
}
this.currentRef = this.viewContainerRef.createComponent(newComponent, {
bindings,
injector: this.viewContainerRef.injector,
});
}
}
private clear() {
this.currentRef?.destroy();
this.currentRef = null;
}
}

View File

@@ -0,0 +1,22 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Types } from '@a2ui/lit/0.8';
import { InjectionToken } from '@angular/core';
export const Theme = new InjectionToken<Theme>('Theme');
export type Theme = Types.Theme;

View File

@@ -0,0 +1,21 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export * from './lib/rendering/index';
export * from './lib/data/index';
export * from './lib/config';
export * from './lib/catalog/default';
export { Surface } from './lib/catalog/surface';

View File

@@ -0,0 +1,23 @@
{
"compileOnSave": false,
"compilerOptions": {
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
}
}

View File

@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.ts"
]
}

2
vendor/a2ui/renderers/lit/.npmrc vendored Normal file
View File

@@ -0,0 +1,2 @@
@a2ui:registry=https://us-npm.pkg.dev/oss-exit-gate-prod/a2ui--npm/
//us-npm.pkg.dev/oss-exit-gate-prod/a2ui--npm/:always-auth=true

9
vendor/a2ui/renderers/lit/README vendored Normal file
View File

@@ -0,0 +1,9 @@
Lit implementation of A2UI.
Important: The sample code provided is for demonstration purposes and illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity.
All operational data received from an external agent—including its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide crafted data in its fields (e.g., name, skills.description) that, if used without sanitization to construct prompts for a Large Language Model (LLM), could expose your application to prompt injection attacks.
Similarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites.
Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users.

9
vendor/a2ui/renderers/lit/README.md vendored Normal file
View File

@@ -0,0 +1,9 @@
Lit implementation of A2UI.
Important: The sample code provided is for demonstration purposes and illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity.
All operational data received from an external agent—including its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide crafted data in its fields (e.g., name, skills.description) that, if used without sanitization to construct prompts for a Large Language Model (LLM), could expose your application to prompt injection attacks.
Similarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites.
Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,752 @@
export * as Events from "./events/events.js";
export * as Types from "./types/types.js";
export * as Primitives from "./types/primitives.js";
export * as Styles from "./styles/index.js";
import * as Guards from "./data/guards.js";
import { create as createSignalA2uiMessageProcessor } from "./data/signal-model-processor.js";
import { A2uiMessageProcessor } from "./data/model-processor.js";
export declare const Data: {
createSignalA2uiMessageProcessor: typeof createSignalA2uiMessageProcessor;
A2uiMessageProcessor: typeof A2uiMessageProcessor;
Guards: typeof Guards;
};
export declare const Schemas: {
A2UIClientEventMessage: {
title: string;
description: string;
type: string;
additionalProperties: boolean;
properties: {
beginRendering: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
surfaceId: {
type: string;
description: string;
};
root: {
type: string;
description: string;
};
styles: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
font: {
type: string;
description: string;
};
primaryColor: {
type: string;
description: string;
pattern: string;
};
};
};
};
required: string[];
};
surfaceUpdate: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
surfaceId: {
type: string;
description: string;
};
components: {
type: string;
description: string;
minItems: number;
items: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
id: {
type: string;
description: string;
};
weight: {
type: string;
description: string;
};
component: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
Text: {
type: string;
additionalProperties: boolean;
properties: {
text: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
usageHint: {
type: string;
description: string;
enum: string[];
};
};
required: string[];
};
Image: {
type: string;
additionalProperties: boolean;
properties: {
url: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
fit: {
type: string;
description: string;
enum: string[];
};
usageHint: {
type: string;
description: string;
enum: string[];
};
};
required: string[];
};
Icon: {
type: string;
additionalProperties: boolean;
properties: {
name: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
enum: string[];
};
path: {
type: string;
};
};
};
};
required: string[];
};
Video: {
type: string;
additionalProperties: boolean;
properties: {
url: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
};
required: string[];
};
AudioPlayer: {
type: string;
additionalProperties: boolean;
properties: {
url: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
description: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
};
required: string[];
};
Row: {
type: string;
additionalProperties: boolean;
properties: {
children: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
explicitList: {
type: string;
items: {
type: string;
};
};
template: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
componentId: {
type: string;
};
dataBinding: {
type: string;
};
};
required: string[];
};
};
};
distribution: {
type: string;
description: string;
enum: string[];
};
alignment: {
type: string;
description: string;
enum: string[];
};
};
required: string[];
};
Column: {
type: string;
additionalProperties: boolean;
properties: {
children: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
explicitList: {
type: string;
items: {
type: string;
};
};
template: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
componentId: {
type: string;
};
dataBinding: {
type: string;
};
};
required: string[];
};
};
};
distribution: {
type: string;
description: string;
enum: string[];
};
alignment: {
type: string;
description: string;
enum: string[];
};
};
required: string[];
};
List: {
type: string;
additionalProperties: boolean;
properties: {
children: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
explicitList: {
type: string;
items: {
type: string;
};
};
template: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
componentId: {
type: string;
};
dataBinding: {
type: string;
};
};
required: string[];
};
};
};
direction: {
type: string;
description: string;
enum: string[];
};
alignment: {
type: string;
description: string;
enum: string[];
};
};
required: string[];
};
Card: {
type: string;
additionalProperties: boolean;
properties: {
child: {
type: string;
description: string;
};
};
required: string[];
};
Tabs: {
type: string;
additionalProperties: boolean;
properties: {
tabItems: {
type: string;
description: string;
items: {
type: string;
additionalProperties: boolean;
properties: {
title: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
child: {
type: string;
};
};
required: string[];
};
};
};
required: string[];
};
Divider: {
type: string;
additionalProperties: boolean;
properties: {
axis: {
type: string;
description: string;
enum: string[];
};
};
};
Modal: {
type: string;
additionalProperties: boolean;
properties: {
entryPointChild: {
type: string;
description: string;
};
contentChild: {
type: string;
description: string;
};
};
required: string[];
};
Button: {
type: string;
additionalProperties: boolean;
properties: {
child: {
type: string;
description: string;
};
primary: {
type: string;
description: string;
};
action: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
name: {
type: string;
};
context: {
type: string;
items: {
type: string;
additionalProperties: boolean;
properties: {
key: {
type: string;
};
value: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
path: {
type: string;
};
literalString: {
type: string;
};
literalNumber: {
type: string;
};
literalBoolean: {
type: string;
};
};
};
};
required: string[];
};
};
};
required: string[];
};
};
required: string[];
};
CheckBox: {
type: string;
additionalProperties: boolean;
properties: {
label: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
value: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalBoolean: {
type: string;
};
path: {
type: string;
};
};
};
};
required: string[];
};
TextField: {
type: string;
additionalProperties: boolean;
properties: {
label: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
text: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
textFieldType: {
type: string;
description: string;
enum: string[];
};
validationRegexp: {
type: string;
description: string;
};
};
required: string[];
};
DateTimeInput: {
type: string;
additionalProperties: boolean;
properties: {
value: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
enableDate: {
type: string;
description: string;
};
enableTime: {
type: string;
description: string;
};
outputFormat: {
type: string;
description: string;
};
};
required: string[];
};
MultipleChoice: {
type: string;
additionalProperties: boolean;
properties: {
selections: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalArray: {
type: string;
items: {
type: string;
};
};
path: {
type: string;
};
};
};
options: {
type: string;
description: string;
items: {
type: string;
additionalProperties: boolean;
properties: {
label: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalString: {
type: string;
};
path: {
type: string;
};
};
};
value: {
type: string;
description: string;
};
};
required: string[];
};
};
maxAllowedSelections: {
type: string;
description: string;
};
};
required: string[];
};
Slider: {
type: string;
additionalProperties: boolean;
properties: {
value: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
literalNumber: {
type: string;
};
path: {
type: string;
};
};
};
minValue: {
type: string;
description: string;
};
maxValue: {
type: string;
description: string;
};
};
required: string[];
};
};
};
};
required: string[];
};
};
};
required: string[];
};
dataModelUpdate: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
surfaceId: {
type: string;
description: string;
};
path: {
type: string;
description: string;
};
contents: {
type: string;
description: string;
items: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
key: {
type: string;
description: string;
};
valueString: {
type: string;
};
valueNumber: {
type: string;
};
valueBoolean: {
type: string;
};
valueMap: {
description: string;
type: string;
items: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
key: {
type: string;
};
valueString: {
type: string;
};
valueNumber: {
type: string;
};
valueBoolean: {
type: string;
};
};
required: string[];
};
};
};
required: string[];
};
};
};
required: string[];
};
deleteSurface: {
type: string;
description: string;
additionalProperties: boolean;
properties: {
surfaceId: {
type: string;
description: string;
};
};
required: string[];
};
};
};
};
//# sourceMappingURL=core.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../../src/0.8/core.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAC;AAC7C,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAC;AAC1C,OAAO,KAAK,UAAU,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAE3C,OAAO,EAAE,MAAM,IAAI,gCAAgC,EAAE,MAAM,kCAAkC,CAAC;AAC9F,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAGjE,eAAO,MAAM,IAAI;;;;CAIhB,CAAC;AAEF,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAEnB,CAAC"}

View File

@@ -0,0 +1,32 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export * as Events from "./events/events.js";
export * as Types from "./types/types.js";
export * as Primitives from "./types/primitives.js";
export * as Styles from "./styles/index.js";
import * as Guards from "./data/guards.js";
import { create as createSignalA2uiMessageProcessor } from "./data/signal-model-processor.js";
import { A2uiMessageProcessor } from "./data/model-processor.js";
import A2UIClientEventMessage from "./schemas/server_to_client_with_standard_catalog.json" with { type: "json" };
export const Data = {
createSignalA2uiMessageProcessor,
A2uiMessageProcessor,
Guards,
};
export const Schemas = {
A2UIClientEventMessage,
};
//# sourceMappingURL=core.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../../src/0.8/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAC;AAC7C,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAC;AAC1C,OAAO,KAAK,UAAU,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,kBAAkB,CAAC;AAE3C,OAAO,EAAE,MAAM,IAAI,gCAAgC,EAAE,MAAM,kCAAkC,CAAC;AAC9F,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,sBAAsB,MAAM,uDAAuD,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAEjH,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,gCAAgC;IAChC,oBAAoB;IACpB,MAAM;CACP,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,sBAAsB;CACvB,CAAC"}

View File

@@ -0,0 +1,24 @@
import { ComponentArrayReference, ResolvedAudioPlayer, ResolvedButton, ResolvedCard, ResolvedCheckbox, ResolvedColumn, ResolvedDateTimeInput, ResolvedDivider, ResolvedIcon, ResolvedImage, ResolvedList, ResolvedModal, ResolvedMultipleChoice, ResolvedRow, ResolvedSlider, ResolvedTabs, ResolvedText, ResolvedTextField, ResolvedVideo, ValueMap } from "../types/types";
export declare function isValueMap(value: unknown): value is ValueMap;
export declare function isPath(key: string, value: unknown): value is string;
export declare function isObject(value: unknown): value is Record<string, unknown>;
export declare function isComponentArrayReference(value: unknown): value is ComponentArrayReference;
export declare function isResolvedAudioPlayer(props: unknown): props is ResolvedAudioPlayer;
export declare function isResolvedButton(props: unknown): props is ResolvedButton;
export declare function isResolvedCard(props: unknown): props is ResolvedCard;
export declare function isResolvedCheckbox(props: unknown): props is ResolvedCheckbox;
export declare function isResolvedColumn(props: unknown): props is ResolvedColumn;
export declare function isResolvedDateTimeInput(props: unknown): props is ResolvedDateTimeInput;
export declare function isResolvedDivider(props: unknown): props is ResolvedDivider;
export declare function isResolvedImage(props: unknown): props is ResolvedImage;
export declare function isResolvedIcon(props: unknown): props is ResolvedIcon;
export declare function isResolvedList(props: unknown): props is ResolvedList;
export declare function isResolvedModal(props: unknown): props is ResolvedModal;
export declare function isResolvedMultipleChoice(props: unknown): props is ResolvedMultipleChoice;
export declare function isResolvedRow(props: unknown): props is ResolvedRow;
export declare function isResolvedSlider(props: unknown): props is ResolvedSlider;
export declare function isResolvedTabs(props: unknown): props is ResolvedTabs;
export declare function isResolvedText(props: unknown): props is ResolvedText;
export declare function isResolvedTextField(props: unknown): props is ResolvedTextField;
export declare function isResolvedVideo(props: unknown): props is ResolvedVideo;
//# sourceMappingURL=guards.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../../src/0.8/data/guards.ts"],"names":[],"mappings":"AAiBA,OAAO,EAEL,uBAAuB,EACvB,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,aAAa,EACb,sBAAsB,EACtB,WAAW,EACX,cAAc,EAEd,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,QAAQ,EACT,MAAM,gBAAgB,CAAC;AAExB,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAE5D;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEnE;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,uBAAuB,CAGlC;AAqCD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,mBAAmB,CAE9B;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAOxE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAcpE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAQ5E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAOxE;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,qBAAqB,CAEhC;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAI1E;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAEtE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAEpE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAOpE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAQtE;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,sBAAsB,CAEjC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAOlE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAExE;AAYD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAOpE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAEpE;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,iBAAiB,CAE5B;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAEtE"}

View File

@@ -0,0 +1,153 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export function isValueMap(value) {
return isObject(value) && "key" in value;
}
export function isPath(key, value) {
return key === "path" && typeof value === "string";
}
export function isObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function isComponentArrayReference(value) {
if (!isObject(value))
return false;
return "explicitList" in value || "template" in value;
}
function isStringValue(value) {
return (isObject(value) &&
("path" in value ||
("literal" in value && typeof value.literal === "string") ||
"literalString" in value));
}
function isNumberValue(value) {
return (isObject(value) &&
("path" in value ||
("literal" in value && typeof value.literal === "number") ||
"literalNumber" in value));
}
function isBooleanValue(value) {
return (isObject(value) &&
("path" in value ||
("literal" in value && typeof value.literal === "boolean") ||
"literalBoolean" in value));
}
function isAnyComponentNode(value) {
if (!isObject(value))
return false;
const hasBaseKeys = "id" in value && "type" in value && "properties" in value;
if (!hasBaseKeys)
return false;
return true;
}
export function isResolvedAudioPlayer(props) {
return isObject(props) && "url" in props && isStringValue(props.url);
}
export function isResolvedButton(props) {
return (isObject(props) &&
"child" in props &&
isAnyComponentNode(props.child) &&
"action" in props);
}
export function isResolvedCard(props) {
if (!isObject(props))
return false;
if (!("child" in props)) {
if (!("children" in props)) {
return false;
}
else {
return (Array.isArray(props.children) &&
props.children.every(isAnyComponentNode));
}
}
return isAnyComponentNode(props.child);
}
export function isResolvedCheckbox(props) {
return (isObject(props) &&
"label" in props &&
isStringValue(props.label) &&
"value" in props &&
isBooleanValue(props.value));
}
export function isResolvedColumn(props) {
return (isObject(props) &&
"children" in props &&
Array.isArray(props.children) &&
props.children.every(isAnyComponentNode));
}
export function isResolvedDateTimeInput(props) {
return isObject(props) && "value" in props && isStringValue(props.value);
}
export function isResolvedDivider(props) {
// Dividers can have all optional properties, so just checking if
// it's an object is enough.
return isObject(props);
}
export function isResolvedImage(props) {
return isObject(props) && "url" in props && isStringValue(props.url);
}
export function isResolvedIcon(props) {
return isObject(props) && "name" in props && isStringValue(props.name);
}
export function isResolvedList(props) {
return (isObject(props) &&
"children" in props &&
Array.isArray(props.children) &&
props.children.every(isAnyComponentNode));
}
export function isResolvedModal(props) {
return (isObject(props) &&
"entryPointChild" in props &&
isAnyComponentNode(props.entryPointChild) &&
"contentChild" in props &&
isAnyComponentNode(props.contentChild));
}
export function isResolvedMultipleChoice(props) {
return isObject(props) && "selections" in props;
}
export function isResolvedRow(props) {
return (isObject(props) &&
"children" in props &&
Array.isArray(props.children) &&
props.children.every(isAnyComponentNode));
}
export function isResolvedSlider(props) {
return isObject(props) && "value" in props && isNumberValue(props.value);
}
function isResolvedTabItem(item) {
return (isObject(item) &&
"title" in item &&
isStringValue(item.title) &&
"child" in item &&
isAnyComponentNode(item.child));
}
export function isResolvedTabs(props) {
return (isObject(props) &&
"tabItems" in props &&
Array.isArray(props.tabItems) &&
props.tabItems.every(isResolvedTabItem));
}
export function isResolvedText(props) {
return isObject(props) && "text" in props && isStringValue(props.text);
}
export function isResolvedTextField(props) {
return isObject(props) && "label" in props && isStringValue(props.label);
}
export function isResolvedVideo(props) {
return isObject(props) && "url" in props && isStringValue(props.url);
}
//# sourceMappingURL=guards.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../../src/0.8/data/guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA4BH,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,KAAK,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,GAAW,EAAE,KAAc;IAChD,OAAO,GAAG,KAAK,MAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,KAAc;IAEd,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,cAAc,IAAI,KAAK,IAAI,UAAU,IAAI,KAAK,CAAC;AACxD,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,CAAC,MAAM,IAAI,KAAK;YACd,CAAC,SAAS,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC;YACzD,eAAe,IAAI,KAAK,CAAC,CAC5B,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,CAAC,MAAM,IAAI,KAAK;YACd,CAAC,SAAS,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC;YACzD,eAAe,IAAI,KAAK,CAAC,CAC5B,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,CAAC,MAAM,IAAI,KAAK;YACd,CAAC,SAAS,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC;YAC1D,gBAAgB,IAAI,KAAK,CAAC,CAC7B,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAc;IACxC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,MAAM,WAAW,GAAG,IAAI,IAAI,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,YAAY,IAAI,KAAK,CAAC;IAC9E,IAAI,CAAC,WAAW;QAAE,OAAO,KAAK,CAAC;IAE/B,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,KAAc;IAEd,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,KAAK,IAAI,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,OAAO,IAAI,KAAK;QAChB,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC;QAC/B,QAAQ,IAAI,KAAK,CAClB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,CAAC,CAAC,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,UAAU,IAAI,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,KAAK,CAAC;QACf,CAAC;aAAM,CAAC;YACN,OAAO,CACL,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;gBAC7B,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,CACzC,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,OAAO,IAAI,KAAK;QAChB,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC;QAC1B,OAAO,IAAI,KAAK;QAChB,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAC5B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,UAAU,IAAI,KAAK;QACnB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC7B,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,CACzC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,KAAc;IAEd,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,IAAI,KAAK,IAAI,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,iEAAiE;IACjE,4BAA4B;IAC5B,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,KAAK,IAAI,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,IAAI,KAAK,IAAI,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,UAAU,IAAI,KAAK;QACnB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC7B,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,CACzC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,iBAAiB,IAAI,KAAK;QAC1B,kBAAkB,CAAC,KAAK,CAAC,eAAe,CAAC;QACzC,cAAc,IAAI,KAAK;QACvB,kBAAkB,CAAC,KAAK,CAAC,YAAY,CAAC,CACvC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,KAAc;IAEd,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,YAAY,IAAI,KAAK,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,UAAU,IAAI,KAAK;QACnB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC7B,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,CACzC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,IAAI,KAAK,IAAI,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC3E,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAa;IACtC,OAAO,CACL,QAAQ,CAAC,IAAI,CAAC;QACd,OAAO,IAAI,IAAI;QACf,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC;QACzB,OAAO,IAAI,IAAI;QACf,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,CAC/B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,CACL,QAAQ,CAAC,KAAK,CAAC;QACf,UAAU,IAAI,KAAK;QACnB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC7B,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,iBAAiB,CAAC,CACxC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,IAAI,KAAK,IAAI,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,KAAc;IAEd,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,IAAI,KAAK,IAAI,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,KAAK,IAAI,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACvE,CAAC"}

View File

@@ -0,0 +1,33 @@
import { ServerToClientMessage, AnyComponentNode, DataValue, Surface, MessageProcessor } from "../types/types";
/**
* Processes and consolidates A2UIProtocolMessage objects into a structured,
* hierarchical model of UI surfaces.
*/
export declare class A2uiMessageProcessor implements MessageProcessor {
#private;
readonly opts: {
mapCtor: MapConstructor;
arrayCtor: ArrayConstructor;
setCtor: SetConstructor;
objCtor: ObjectConstructor;
};
static readonly DEFAULT_SURFACE_ID = "@default";
constructor(opts?: {
mapCtor: MapConstructor;
arrayCtor: ArrayConstructor;
setCtor: SetConstructor;
objCtor: ObjectConstructor;
});
getSurfaces(): ReadonlyMap<string, Surface>;
clearSurfaces(): void;
processMessages(messages: ServerToClientMessage[]): void;
/**
* Retrieves the data for a given component node and a relative path string.
* This correctly handles the special `.` path, which refers to the node's
* own data context.
*/
getData(node: AnyComponentNode, relativePath: string, surfaceId?: string): DataValue | null;
setData(node: AnyComponentNode | null, relativePath: string, value: DataValue, surfaceId?: string): void;
resolvePath(path: string, dataContextPath?: string): string;
}
//# sourceMappingURL=model-processor.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"model-processor.d.ts","sourceRoot":"","sources":["../../../../src/0.8/data/model-processor.ts"],"names":[],"mappings":"AAgBA,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAKhB,SAAS,EAIT,OAAO,EAGP,gBAAgB,EAGjB,MAAM,gBAAgB,CAAC;AA0BxB;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,gBAAgB;;IAUzD,QAAQ,CAAC,IAAI,EAAE;QACb,OAAO,EAAE,cAAc,CAAC;QACxB,SAAS,EAAE,gBAAgB,CAAC;QAC5B,OAAO,EAAE,cAAc,CAAC;QACxB,OAAO,EAAE,iBAAiB,CAAC;KAC5B;IAdH,MAAM,CAAC,QAAQ,CAAC,kBAAkB,cAAc;gBASrC,IAAI,GAAE;QACb,OAAO,EAAE,cAAc,CAAC;QACxB,SAAS,EAAE,gBAAgB,CAAC;QAC5B,OAAO,EAAE,cAAc,CAAC;QACxB,OAAO,EAAE,iBAAiB,CAAC;KACwC;IAUvE,WAAW,IAAI,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC;IAI3C,aAAa;IAIb,eAAe,CAAC,QAAQ,EAAE,qBAAqB,EAAE,GAAG,IAAI;IA6BxD;;;;OAIG;IACH,OAAO,CACL,IAAI,EAAE,gBAAgB,EACtB,YAAY,EAAE,MAAM,EACpB,SAAS,SAA0C,GAClD,SAAS,GAAG,IAAI;IAkBnB,OAAO,CACL,IAAI,EAAE,gBAAgB,GAAG,IAAI,EAC7B,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,SAAS,EAChB,SAAS,SAA0C,GAClD,IAAI;IAuBP,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM;CAkqB5D"}

View File

@@ -0,0 +1,628 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { isComponentArrayReference, isObject, isPath, isResolvedAudioPlayer, isResolvedButton, isResolvedCard, isResolvedCheckbox, isResolvedColumn, isResolvedDateTimeInput, isResolvedDivider, isResolvedIcon, isResolvedImage, isResolvedList, isResolvedModal, isResolvedMultipleChoice, isResolvedRow, isResolvedSlider, isResolvedTabs, isResolvedText, isResolvedTextField, isResolvedVideo, } from "./guards.js";
/**
* Processes and consolidates A2UIProtocolMessage objects into a structured,
* hierarchical model of UI surfaces.
*/
export class A2uiMessageProcessor {
static { this.DEFAULT_SURFACE_ID = "@default"; }
#mapCtor = Map;
#arrayCtor = Array;
#setCtor = Set;
#objCtor = Object;
#surfaces;
constructor(opts = { mapCtor: Map, arrayCtor: Array, setCtor: Set, objCtor: Object }) {
this.opts = opts;
this.#arrayCtor = opts.arrayCtor;
this.#mapCtor = opts.mapCtor;
this.#setCtor = opts.setCtor;
this.#objCtor = opts.objCtor;
this.#surfaces = new opts.mapCtor();
}
getSurfaces() {
return this.#surfaces;
}
clearSurfaces() {
this.#surfaces.clear();
}
processMessages(messages) {
for (const message of messages) {
if (message.beginRendering) {
this.#handleBeginRendering(message.beginRendering, message.beginRendering.surfaceId);
}
if (message.surfaceUpdate) {
this.#handleSurfaceUpdate(message.surfaceUpdate, message.surfaceUpdate.surfaceId);
}
if (message.dataModelUpdate) {
this.#handleDataModelUpdate(message.dataModelUpdate, message.dataModelUpdate.surfaceId);
}
if (message.deleteSurface) {
this.#handleDeleteSurface(message.deleteSurface);
}
}
}
/**
* Retrieves the data for a given component node and a relative path string.
* This correctly handles the special `.` path, which refers to the node's
* own data context.
*/
getData(node, relativePath, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID) {
const surface = this.#getOrCreateSurface(surfaceId);
if (!surface)
return null;
let finalPath;
// The special `.` path means the final path is the node's data context
// path and so we return the dataContextPath as-is.
if (relativePath === "." || relativePath === "") {
finalPath = node.dataContextPath ?? "/";
}
else {
// For all other paths, resolve them against the node's context.
finalPath = this.resolvePath(relativePath, node.dataContextPath);
}
return this.#getDataByPath(surface.dataModel, finalPath);
}
setData(node, relativePath, value, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID) {
if (!node) {
console.warn("No component node set");
return;
}
const surface = this.#getOrCreateSurface(surfaceId);
if (!surface)
return;
let finalPath;
// The special `.` path means the final path is the node's data context
// path and so we return the dataContextPath as-is.
if (relativePath === "." || relativePath === "") {
finalPath = node.dataContextPath ?? "/";
}
else {
// For all other paths, resolve them against the node's context.
finalPath = this.resolvePath(relativePath, node.dataContextPath);
}
this.#setDataByPath(surface.dataModel, finalPath, value);
}
resolvePath(path, dataContextPath) {
// If the path is absolute, it overrides any context.
if (path.startsWith("/")) {
return path;
}
if (dataContextPath && dataContextPath !== "/") {
// Ensure there's exactly one slash between the context and the path.
return dataContextPath.endsWith("/")
? `${dataContextPath}${path}`
: `${dataContextPath}/${path}`;
}
// Fallback for no context or root context: make it an absolute path.
return `/${path}`;
}
#parseIfJsonString(value) {
if (typeof value !== "string") {
return value;
}
const trimmedValue = value.trim();
if ((trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) ||
(trimmedValue.startsWith("[") && trimmedValue.endsWith("]"))) {
try {
// It looks like JSON, attempt to parse it.
return JSON.parse(value);
}
catch (e) {
// It looked like JSON but wasn't. Keep the original string.
console.warn(`Failed to parse potential JSON string: "${value.substring(0, 50)}..."`, e);
return value; // Return original string
}
}
// It's a string, but not JSON-like.
return value;
}
/**
* Converts a specific array format [{key: "...", value_string: "..."}, ...]
* into a standard Map. It also attempts to parse any string values that
* appear to be stringified JSON.
*/
#convertKeyValueArrayToMap(arr) {
const map = new this.#mapCtor();
for (const item of arr) {
if (!isObject(item) || !("key" in item))
continue;
const key = item.key;
// Find the value, which is in a property prefixed with "value".
const valueKey = this.#findValueKey(item);
if (!valueKey)
continue;
let value = item[valueKey];
// It's a valueMap. We must recursively convert it.
if (valueKey === "valueMap" && Array.isArray(value)) {
value = this.#convertKeyValueArrayToMap(value);
}
else if (typeof value === "string") {
value = this.#parseIfJsonString(value);
}
this.#setDataByPath(map, key, value);
}
return map;
}
#setDataByPath(root, path, value) {
// Check if the incoming value is the special key-value array format.
if (Array.isArray(value) &&
(value.length === 0 || (isObject(value[0]) && "key" in value[0]))) {
// Check for "set primitive at path" convention:
// path: "/messages/123", contents: [{ key: ".", valueString: "hi" }]
if (value.length === 1 && isObject(value[0]) && value[0].key === ".") {
const item = value[0];
const valueKey = this.#findValueKey(item);
if (valueKey) {
// Extract the primitive value
value = item[valueKey];
// We must still process this value in case it's a valueMap or
// a JSON string.
if (valueKey === "valueMap" && Array.isArray(value)) {
value = this.#convertKeyValueArrayToMap(value);
}
else if (typeof value === "string") {
value = this.#parseIfJsonString(value);
}
// Now, `value` is the primitive (e.g., "hi"), and we continue
// the function.
}
else {
// Malformed, but fall back to existing behavior.
value = this.#convertKeyValueArrayToMap(value);
}
}
else {
value = this.#convertKeyValueArrayToMap(value);
}
}
const segments = this.#normalizePath(path)
.split("/")
.filter((s) => s);
if (segments.length === 0) {
// Root data can either be a Map or an Object. If we receive an Object,
// however, we will normalize it to a proper Map.
if (value instanceof Map || isObject(value)) {
// Normalize an Object to a Map.
if (!(value instanceof Map) && isObject(value)) {
value = new this.#mapCtor(Object.entries(value));
}
root.clear();
for (const [key, v] of value.entries()) {
root.set(key, v);
}
}
else {
console.error("Cannot set root of DataModel to a non-Map value.");
}
return;
}
let current = root;
for (let i = 0; i < segments.length - 1; i++) {
const segment = segments[i];
let target;
if (current instanceof Map) {
target = current.get(segment);
}
else if (Array.isArray(current) && /^\d+$/.test(segment)) {
target = current[parseInt(segment, 10)];
}
if (target === undefined ||
typeof target !== "object" ||
target === null) {
target = new this.#mapCtor();
if (current instanceof this.#mapCtor) {
current.set(segment, target);
}
else if (Array.isArray(current)) {
current[parseInt(segment, 10)] = target;
}
}
current = target;
}
const finalSegment = segments[segments.length - 1];
const storedValue = value;
if (current instanceof this.#mapCtor) {
current.set(finalSegment, storedValue);
}
else if (Array.isArray(current) && /^\d+$/.test(finalSegment)) {
current[parseInt(finalSegment, 10)] = storedValue;
}
}
/**
* Normalizes a path string into a consistent, slash-delimited format.
* Converts bracket notation and dot notation in a two-pass.
* e.g., "bookRecommendations[0].title" -> "/bookRecommendations/0/title"
* e.g., "book.0.title" -> "/book/0/title"
*/
#normalizePath(path) {
// 1. Replace all bracket accessors `[index]` with dot accessors `.index`
const dotPath = path.replace(/\[(\d+)\]/g, ".$1");
// 2. Split by dots
const segments = dotPath.split(".");
// 3. Join with slashes and ensure it starts with a slash
return "/" + segments.filter((s) => s.length > 0).join("/");
}
#getDataByPath(root, path) {
const segments = this.#normalizePath(path)
.split("/")
.filter((s) => s);
let current = root;
for (const segment of segments) {
if (current === undefined || current === null)
return null;
if (current instanceof Map) {
current = current.get(segment);
}
else if (Array.isArray(current) && /^\d+$/.test(segment)) {
current = current[parseInt(segment, 10)];
}
else if (isObject(current)) {
current = current[segment];
}
else {
// If we need to traverse deeper but `current` is a primitive, the path is invalid.
return null;
}
}
return current;
}
#getOrCreateSurface(surfaceId) {
let surface = this.#surfaces.get(surfaceId);
if (!surface) {
surface = new this.#objCtor({
rootComponentId: null,
componentTree: null,
dataModel: new this.#mapCtor(),
components: new this.#mapCtor(),
styles: new this.#objCtor(),
});
this.#surfaces.set(surfaceId, surface);
}
return surface;
}
#handleBeginRendering(message, surfaceId) {
const surface = this.#getOrCreateSurface(surfaceId);
surface.rootComponentId = message.root;
surface.styles = message.styles ?? {};
this.#rebuildComponentTree(surface);
}
#handleSurfaceUpdate(message, surfaceId) {
const surface = this.#getOrCreateSurface(surfaceId);
for (const component of message.components) {
surface.components.set(component.id, component);
}
this.#rebuildComponentTree(surface);
}
#handleDataModelUpdate(message, surfaceId) {
const surface = this.#getOrCreateSurface(surfaceId);
const path = message.path ?? "/";
this.#setDataByPath(surface.dataModel, path, message.contents);
this.#rebuildComponentTree(surface);
}
#handleDeleteSurface(message) {
this.#surfaces.delete(message.surfaceId);
}
/**
* Starts at the root component of the surface and builds out the tree
* recursively. This process involves resolving all properties of the child
* components, and expanding on any explicit children lists or templates
* found in the structure.
*
* @param surface The surface to be built.
*/
#rebuildComponentTree(surface) {
if (!surface.rootComponentId) {
surface.componentTree = null;
return;
}
// Track visited nodes to avoid circular references.
const visited = new this.#setCtor();
surface.componentTree = this.#buildNodeRecursive(surface.rootComponentId, surface, visited, "/", "" // Initial idSuffix.
);
}
/** Finds a value key in a map. */
#findValueKey(value) {
return Object.keys(value).find((k) => k.startsWith("value"));
}
/**
* Builds out the nodes recursively.
*/
#buildNodeRecursive(baseComponentId, surface, visited, dataContextPath, idSuffix = "") {
const fullId = `${baseComponentId}${idSuffix}`; // Construct the full ID
const { components } = surface;
if (!components.has(baseComponentId)) {
return null;
}
if (visited.has(fullId)) {
throw new Error(`Circular dependency for component "${fullId}".`);
}
visited.add(fullId);
const componentData = components.get(baseComponentId);
const componentProps = componentData.component ?? {};
const componentType = Object.keys(componentProps)[0];
const unresolvedProperties = componentProps[componentType];
// Manually build the resolvedProperties object by resolving each value in
// the component's properties.
const resolvedProperties = new this.#objCtor();
if (isObject(unresolvedProperties)) {
for (const [key, value] of Object.entries(unresolvedProperties)) {
resolvedProperties[key] = this.#resolvePropertyValue(value, surface, visited, dataContextPath, idSuffix);
}
}
visited.delete(fullId);
// Now that we have the resolved properties in place we can go ahead and
// ensure that they meet expectations in terms of types and so forth,
// casting them into the specific shape for usage.
const baseNode = {
id: fullId,
dataContextPath,
weight: componentData.weight ?? "initial",
};
switch (componentType) {
case "Text":
if (!isResolvedText(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Text",
properties: resolvedProperties,
});
case "Image":
if (!isResolvedImage(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Image",
properties: resolvedProperties,
});
case "Icon":
if (!isResolvedIcon(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Icon",
properties: resolvedProperties,
});
case "Video":
if (!isResolvedVideo(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Video",
properties: resolvedProperties,
});
case "AudioPlayer":
if (!isResolvedAudioPlayer(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "AudioPlayer",
properties: resolvedProperties,
});
case "Row":
if (!isResolvedRow(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Row",
properties: resolvedProperties,
});
case "Column":
if (!isResolvedColumn(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Column",
properties: resolvedProperties,
});
case "List":
if (!isResolvedList(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "List",
properties: resolvedProperties,
});
case "Card":
if (!isResolvedCard(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Card",
properties: resolvedProperties,
});
case "Tabs":
if (!isResolvedTabs(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Tabs",
properties: resolvedProperties,
});
case "Divider":
if (!isResolvedDivider(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Divider",
properties: resolvedProperties,
});
case "Modal":
if (!isResolvedModal(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Modal",
properties: resolvedProperties,
});
case "Button":
if (!isResolvedButton(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Button",
properties: resolvedProperties,
});
case "CheckBox":
if (!isResolvedCheckbox(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "CheckBox",
properties: resolvedProperties,
});
case "TextField":
if (!isResolvedTextField(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "TextField",
properties: resolvedProperties,
});
case "DateTimeInput":
if (!isResolvedDateTimeInput(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "DateTimeInput",
properties: resolvedProperties,
});
case "MultipleChoice":
if (!isResolvedMultipleChoice(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "MultipleChoice",
properties: resolvedProperties,
});
case "Slider":
if (!isResolvedSlider(resolvedProperties)) {
throw new Error(`Invalid data; expected ${componentType}`);
}
return new this.#objCtor({
...baseNode,
type: "Slider",
properties: resolvedProperties,
});
default:
// Catch-all for other custom component types.
return new this.#objCtor({
...baseNode,
type: componentType,
properties: resolvedProperties,
});
}
}
/**
* Recursively resolves an individual property value. If a property indicates
* a child node (a string that matches a component ID), an explicitList of
* children, or a template, these will be built out here.
*/
#resolvePropertyValue(value, surface, visited, dataContextPath, idSuffix = "") {
// 1. If it's a string that matches a component ID, build that node.
if (typeof value === "string" && surface.components.has(value)) {
return this.#buildNodeRecursive(value, surface, visited, dataContextPath, idSuffix);
}
// 2. If it's a ComponentArrayReference (e.g., a `children` property),
// resolve the list and return an array of nodes.
if (isComponentArrayReference(value)) {
if (value.explicitList) {
return value.explicitList.map((id) => this.#buildNodeRecursive(id, surface, visited, dataContextPath, idSuffix));
}
if (value.template) {
const fullDataPath = this.resolvePath(value.template.dataBinding, dataContextPath);
const data = this.#getDataByPath(surface.dataModel, fullDataPath);
const template = value.template;
// Handle Array data.
if (Array.isArray(data)) {
return data.map((_, index) => {
// Create a synthetic ID based on the template ID and the
// full index path of the data (e.g., template-id:0:1)
const parentIndices = dataContextPath
.split("/")
.filter((segment) => /^\d+$/.test(segment));
const newIndices = [...parentIndices, index];
const newSuffix = `:${newIndices.join(":")}`;
const childDataContextPath = `${fullDataPath}/${index}`;
return this.#buildNodeRecursive(template.componentId, // baseId
surface, visited, childDataContextPath, newSuffix // new suffix
);
});
}
// Handle Map data.
const mapCtor = this.#mapCtor;
if (data instanceof mapCtor) {
return Array.from(data.keys(), (key) => {
const newSuffix = `:${key}`;
const childDataContextPath = `${fullDataPath}/${key}`;
return this.#buildNodeRecursive(template.componentId, // baseId
surface, visited, childDataContextPath, newSuffix // new suffix
);
});
}
// Return empty array if the data is not ready yet.
return new this.#arrayCtor();
}
}
// 3. If it's a plain array, resolve each of its items.
if (Array.isArray(value)) {
return value.map((item) => this.#resolvePropertyValue(item, surface, visited, dataContextPath, idSuffix));
}
// 4. If it's a plain object, resolve each of its properties.
if (isObject(value)) {
const newObj = new this.#objCtor();
for (const [key, propValue] of Object.entries(value)) {
// Special case for paths. Here we might get /item/ or ./ on the front
// of the path which isn't what we want. In this case we check the
// dataContextPath and if 1) it's not the default and 2) we also see the
// path beginning with /item/ or ./we trim it.
let propertyValue = propValue;
if (isPath(key, propValue) && dataContextPath !== "/") {
propertyValue = propValue
.replace(/^\.?\/item/, "")
.replace(/^\.?\/text/, "")
.replace(/^\.?\/label/, "")
.replace(/^\.?\//, "");
newObj[key] = propertyValue;
continue;
}
newObj[key] = this.#resolvePropertyValue(propertyValue, surface, visited, dataContextPath, idSuffix);
}
return newObj;
}
// 5. Otherwise, it's a primitive value.
return value;
}
}
//# sourceMappingURL=model-processor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import { A2uiMessageProcessor } from "./model-processor.js";
export declare function create(): A2uiMessageProcessor;
//# sourceMappingURL=signal-model-processor.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"signal-model-processor.d.ts","sourceRoot":"","sources":["../../../../src/0.8/data/signal-model-processor.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAO5D,wBAAgB,MAAM,yBAOrB"}

View File

@@ -0,0 +1,29 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { A2uiMessageProcessor } from "./model-processor.js";
import { SignalArray } from "signal-utils/array";
import { SignalMap } from "signal-utils/map";
import { SignalObject } from "signal-utils/object";
import { SignalSet } from "signal-utils/set";
export function create() {
return new A2uiMessageProcessor({
arrayCtor: SignalArray,
mapCtor: SignalMap,
objCtor: SignalObject,
setCtor: SignalSet,
});
}
//# sourceMappingURL=signal-model-processor.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"signal-model-processor.js","sourceRoot":"","sources":["../../../../src/0.8/data/signal-model-processor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,UAAU,MAAM;IACpB,OAAO,IAAI,oBAAoB,CAAC;QAC9B,SAAS,EAAE,WAA0C;QACrD,OAAO,EAAE,SAAsC;QAC/C,OAAO,EAAE,YAA4C;QACrD,OAAO,EAAE,SAAsC;KAChD,CAAC,CAAC;AACL,CAAC"}

View File

@@ -0,0 +1,12 @@
import { Action } from "../types/components.js";
import { AnyComponentNode } from "../types/types.js";
import { BaseEventDetail } from "./base.js";
type Namespace = "a2ui";
export interface A2UIAction extends BaseEventDetail<`${Namespace}.action`> {
readonly action: Action;
readonly dataContextPath: string;
readonly sourceComponentId: string;
readonly sourceComponent: AnyComponentNode | null;
}
export {};
//# sourceMappingURL=a2ui.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"a2ui.d.ts","sourceRoot":"","sources":["../../../../src/0.8/events/a2ui.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,KAAK,SAAS,GAAG,MAAM,CAAC;AAExB,MAAM,WAAW,UAAW,SAAQ,eAAe,CAAC,GAAG,SAAS,SAAS,CAAC;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,eAAe,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACnD"}

View File

@@ -0,0 +1,17 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export {};
//# sourceMappingURL=a2ui.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"a2ui.js","sourceRoot":"","sources":["../../../../src/0.8/events/a2ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}

View File

@@ -0,0 +1,4 @@
export interface BaseEventDetail<EventType extends string> {
readonly eventType: EventType;
}
//# sourceMappingURL=base.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/0.8/events/base.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,eAAe,CAAC,SAAS,SAAS,MAAM;IACvD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;CAC/B"}

View File

@@ -0,0 +1,17 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export {};
//# sourceMappingURL=base.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"base.js","sourceRoot":"","sources":["../../../../src/0.8/events/base.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}

View File

@@ -0,0 +1,20 @@
import type * as A2UI from "./a2ui.js";
import { BaseEventDetail } from "./base.js";
type EnforceEventTypeMatch<T extends Record<string, BaseEventDetail<string>>> = {
[K in keyof T]: T[K] extends BaseEventDetail<infer EventType> ? EventType extends K ? T[K] : never : never;
};
export type StateEventDetailMap = EnforceEventTypeMatch<{
"a2ui.action": A2UI.A2UIAction;
}>;
export declare class StateEvent<T extends keyof StateEventDetailMap> extends CustomEvent<StateEventDetailMap[T]> {
readonly payload: StateEventDetailMap[T];
static eventName: string;
constructor(payload: StateEventDetailMap[T]);
}
declare global {
interface HTMLElementEventMap {
a2uiaction: StateEvent<"a2ui.action">;
}
}
export {};
//# sourceMappingURL=events.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../../src/0.8/events/events.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAQ5C,KAAK,qBAAqB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC,IAC1E;KACG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,eAAe,CAAC,MAAM,SAAS,CAAC,GACzD,SAAS,SAAS,CAAC,GACjB,CAAC,CAAC,CAAC,CAAC,GACJ,KAAK,GACP,KAAK;CACV,CAAC;AAEJ,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;IACtD,aAAa,EAAE,IAAI,CAAC,UAAU,CAAC;CAChC,CAAC,CAAC;AAEH,qBAAa,UAAU,CACrB,CAAC,SAAS,MAAM,mBAAmB,CACnC,SAAQ,WAAW,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;IAG/B,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAFpD,MAAM,CAAC,SAAS,SAAgB;gBAEX,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC;CAGrD;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,mBAAmB;QAC3B,UAAU,EAAE,UAAU,CAAC,aAAa,CAAC,CAAC;KACvC;CACF"}

View File

@@ -0,0 +1,28 @@
/*
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const eventInit = {
bubbles: true,
cancelable: true,
composed: true,
};
export class StateEvent extends CustomEvent {
static { this.eventName = "a2uiaction"; }
constructor(payload) {
super(StateEvent.eventName, { detail: payload, ...eventInit });
this.payload = payload;
}
}
//# sourceMappingURL=events.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../../src/0.8/events/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,MAAM,SAAS,GAAG;IAChB,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,IAAI;IAChB,QAAQ,EAAE,IAAI;CACf,CAAC;AAeF,MAAM,OAAO,UAEX,SAAQ,WAAmC;aACpC,cAAS,GAAG,YAAY,CAAC;IAEhC,YAAqB,OAA+B;QAClD,KAAK,CAAC,UAAU,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,EAAE,CAAC,CAAC;QAD5C,YAAO,GAAP,OAAO,CAAwB;IAEpD,CAAC"}

Some files were not shown because too many files have changed in this diff Show More