Input Agnostic Player and Native A2UI Support#866
Draft
KetanReddy wants to merge 21 commits into
Draft
Conversation
Member
Author
|
/docs |
Bundle ReportChanges will increase total bundle size by 640.55kB (11.39%) ⬆️
Affected Assets, Files, and Routes:view changes for bundle: plugins/markdown/coreAssets Changed:
view changes for bundle: core/playerAssets Changed:
view changes for bundle: plugins/async-node/coreAssets Changed:
view changes for bundle: plugins/check-path/coreAssets Changed:
view changes for bundle: plugins/common-expressions/coreAssets Changed:
view changes for bundle: plugins/common-types/coreAssets Changed:
view changes for bundle: plugins/reference-assets/coreAssets Changed:
view changes for bundle: plugins/stage-revert-data/coreAssets Changed:
view changes for bundle: plugins/beacon/coreAssets Changed:
view changes for bundle: plugins/metrics/coreAssets Changed:
view changes for bundle: tools/storybookAssets Changed:
view changes for bundle: react/playerAssets Changed:
|
intuit-svc
added a commit
to player-ui/player-ui.github.io
that referenced
this pull request
May 12, 2026
Contributor
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #866 +/- ##
===========================
===========================
☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Member
Author
|
/canary |
intuit-svc
added a commit
to player-ui/player-ui.github.io
that referenced
this pull request
May 12, 2026
Contributor
…ion logic to plugin. Also migrate to Bazel Repo based node module ignore
Member
Author
|
/canary |
intuit-svc
added a commit
to player-ui/player-ui.github.io
that referenced
this pull request
May 22, 2026
Contributor
Benchmark ResultsComparison against baseline from
|
| Benchmark | Current | Baseline | Change |
|---|---|---|---|
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.bar |
848.00K ops/s | 814.11K ops/s | +4.2% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets.1.name |
547.02K ops/s | 520.99K ops/s | +5.0% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets.01.name |
449.12K ops/s | 484.39K ops/s | -7.3% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets['01'].name |
498.53K ops/s | 443.70K ops/s | +12.4% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets[01].name |
532.39K ops/s | 448.52K ops/s | +18.7% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets[name = "frodo"].type |
304.09K ops/s | 229.59K ops/s | +32.4% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["name" = "sprinkles"].type |
234.41K ops/s | 236.56K ops/s | -0.9% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["isDog" = false].type |
298.19K ops/s | 293.76K ops/s | +1.5% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["isDog" = true].type |
300.65K ops/s | 291.77K ops/s | +3.0% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.bar |
578.89K ops/s | 585.10K ops/s | -1.1% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets.1.name |
389.57K ops/s | 362.44K ops/s | +7.5% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets.01.name |
394.17K ops/s | 365.41K ops/s | +7.9% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets['01'].name |
350.32K ops/s | 337.57K ops/s | +3.8% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets[01].name |
367.57K ops/s | 360.75K ops/s | +1.9% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets[name = "frodo"].type |
240.45K ops/s | 231.82K ops/s | +3.7% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["name" = "sprinkles"].type |
197.40K ops/s | 195.10K ops/s | +1.2% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["isDog" = false].type |
241.22K ops/s | 234.64K ops/s | +2.8% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["isDog" = true].type |
238.75K ops/s | 233.18K ops/s | +2.4% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = 1 + 3 (sync) |
457.16K ops/s | 386.32K ops/s | +18.3% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = 1 + 3 (async) |
368.63K ops/s | 355.63K ops/s | +3.7% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: conditional(true, true, false) (sync) |
478.84K ops/s | 467.51K ops/s | +2.4% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: conditional(true, true, false) (async) |
412.61K ops/s | 427.69K ops/s | -3.5% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional({{bar}} > 0, true, false) (sync) |
190.30K ops/s | 141.47K ops/s | +34.5% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional({{bar}} > 0, true, false) (async) |
192.94K ops/s | 146.09K ops/s | +32.1% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(conditional(true = false, false, true), conditional(false = false, true, false), conditional(true = true, false, true)) (sync) |
133.81K ops/s | 140.98K ops/s | -5.1% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(conditional(true = false, false, true), conditional(false = false, true, false), conditional(true = true, false, true)) (async) |
148.63K ops/s | 151.43K ops/s | -1.9% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = await(asyncTestFunction(1)) (sync) |
N/A | N/A | N/A |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = await(asyncTestFunction(1)) (async) |
263.35K ops/s | 261.47K ops/s | +0.7% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = asyncTestFunction(1) (sync) |
293.14K ops/s | 345.63K ops/s | -15.2% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = asyncTestFunction(1) (async) |
283.15K ops/s | 269.48K ops/s | +5.1% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: asyncTestFunction(1) (sync) |
749.11K ops/s | 834.09K ops/s | -10.2% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: asyncTestFunction(1) (async) |
633.39K ops/s | 640.68K ops/s | -1.1% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(!{{bar}} == false, await(asyncTestFunction(1)), false) (sync) |
168.04K ops/s | 161.30K ops/s | +4.2% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(!{{bar}} == false, await(asyncTestFunction(1)), false) (async) |
151.77K ops/s | 158.47K ops/s | -4.2% |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > initial resolve |
601.65 ops/s | 545.34 ops/s | +10.3% ✅ |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > Resolving from cache |
15.39K ops/s | 15.42K ops/s | -0.2% |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > data changes |
2.65K ops/s | 2.13K ops/s | +24.3% ✅ |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > data changes slow |
583.76 ops/s | 406.30 ops/s | +43.7% ✅ |
plugins/async-node/core ⚠️
| Benchmark | Current | Baseline | Change |
|---|---|---|---|
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 1 times |
6.82K ops/s | 8.39K ops/s | -18.8% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 5 times |
8.23K ops/s | 10.87K ops/s | -24.3% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 10 times |
6.70K ops/s | 8.74K ops/s | -23.3% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 50 times |
2.69K ops/s | 2.48K ops/s | +8.5% ✅ |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 100 times |
1.67K ops/s | 1.40K ops/s | +19.4% ✅ |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 1 times |
7.28K ops/s | 7.81K ops/s | -6.8% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 5 times |
7.64K ops/s | 7.80K ops/s | -2.1% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 10 times |
6.11K ops/s | 7.66K ops/s | -20.2% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 50 times |
2.20K ops/s | 2.53K ops/s | -13.4% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 100 times |
1.24K ops/s | 1.43K ops/s | -13.4% |
react/player
| Benchmark | Current | Baseline | Change |
|---|---|---|---|
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 1 ReactAssets |
598.73 ops/s | 572.33 ops/s | +4.6% |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 1 ReactAssets |
1.13K ops/s | 895.15 ops/s | +25.7% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 5 ReactAssets |
603.87 ops/s | 564.93 ops/s | +6.9% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 5 ReactAssets |
1.09K ops/s | 888.75 ops/s | +23.2% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 10 ReactAssets |
564.50 ops/s | 536.68 ops/s | +5.2% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 10 ReactAssets |
899.54 ops/s | 805.32 ops/s | +11.7% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 50 ReactAssets |
460.31 ops/s | 389.77 ops/s | +18.1% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 50 ReactAssets |
239.57 ops/s | 208.73 ops/s | +14.8% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 100 ReactAssets |
344.69 ops/s | 282.34 ops/s | +22.1% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 100 ReactAssets |
111.29 ops/s | 92.51 ops/s | +20.3% ✅ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and Why
With the emergence of A2UI and other agent-driven standards, Player — in its mission to serve the broader SDUI space — should expand support for more formats beyond its own in order to broaden the scope of what Player solves for. Furthermore, this lets capabilities be built against Player itself, regardless of input format, allowing for the decoupling of implementation from the server driving it.
This PR adds the generic "unknown format" entrypoint to every platform (web, JVM, Android, iOS) and ships A2UI as the first non-Player format end-to-end.
Core Player Changes
transformContentwaterfall hook on Player's hooks. Fires at the top ofstart(), after plugins are registered and beforeresolveFlowContent. Plugins inspect aContentMeta{ format, version }and either convert the payload or pass it through.Player.start()signature widened from(payload: Flow)to(payload: unknown, options?: StartOptions). Default format is"player", which preserves existing behavior (a plain Flow flows through untouched).versionis free-form, so a single format plugin can dispatch across versions.Platform Entrypoints
The hook and signature widening live in the core JS bundle every platform loads; each platform entrypoint forwards the
format/versionthrough. Default"player"keeps all existing call sites untouched.ReactPlayer.start()mirrors the same signature (react/player/src/player.tsx) and forwards options to the underlyingPlayer.start().Player.start()gainsstart(flow: String, format: String, version: String? = null)(plus a matchingURLoverload).HeadlessPlayerimplements it and forwards{ format, version }to the JSplayer.start(payload, options)as a bridge-encodedMap.AndroidPlayer.start(flow, format, version)delegates to the wrappedHeadlessPlayer.StartOptions { format, version }value type;HeadlessPlayer.start(flow:options:completion:)appends the options object to the JSstartargs ([String: Any]→ JS object via JavaScriptCore). Threaded throughSwiftUIPlayer.init/Context.load/ManagedPlayervia a defaultedstartOptions:param (source-compatible). UseStartOptions.a2ui.Plugins
Core:
@player-ui/a2ui-pluginIncludes three sub-plugins:
A2UIContentPlugin: Only activates whenmeta.format === "a2ui". CallsadaptA2UIToFlow(snapshot)to transform content.A2UITransformPlugin: Per-asset transforms (Button/TextField/CheckBox/Slider/DateTimeInput/ChoicePicker/Text) that attachrun()/set()/valuehelpers consumed by the rendering layer.A2UIExpressionsPlugin: Registers the A2UI v0.9.1 standard library: validation (required,regex,length,numeric,email), formatters (formatString,formatNumber,formatCurrency,formatDate,pluralize),openUrl, and logic (and/or/not).This package emits a native JS bundle (
A2UIPlugin.native.js), which the JVM and iOS wrappers below load directly — so the adapter, transforms, and expression std-lib are shared across all platforms with zero reimplementation.Adapter Logic
Walks the flat
components[]list fromid: "root", inlines child references into a nested asset tree matching Player's{asset: ...}shape, and produces a Flow with a single VIEW state plus one END per unique event name encountered. Translation rules:{path: "/x/y"}→"x.y"(Player binding)formatString(...)→"Hello, {{x.y}}!"template{call, args}→@[fn(...)]@expressionchecks: [...]on inputs → lifted into a synthesizedFlow.schemaviasynthesizeSchemaso Player's existing SchemaController/ValidationController pick them up unchanged{path, componentId}blocks become indexed paths scoped to<scope>._index_Platform Renders
The A2UI v0.9.1 reference catalog — 16 assets — implemented per platform: Row, Column, List, Text, Image, Icon, Divider, Card, Modal, Tabs, Button, TextField, CheckBox, Slider, DateTimeInput, ChoicePicker. Asset
typestrings are PascalCase to match the adapter output; transformed assets consume the sharedrun()/set()/currentValuehelpers.@player-ui/a2ui-plugin-react— the full catalog as React components.plugins/a2ui/android—A2UIPlugin : AndroidPlayerPlugin, JSPluginWrapper by <jvm wrapper>registering the catalog as Jetpack Compose renderers. Function helpers decode as Kotlin function types.plugins/a2ui/swiftui—A2UIPlugin : JSBasePlugin, NativePluginthat loads the bundle and registers the catalog as SwiftUI renderers. Function helpers decode as SwiftWrappedFunctions.plugins/a2ui/jvm— generated KotlinJSPluginWrapper(A2UIPlugin) that loads the A2UI JS bundle (mirrorsreference-assets/jvm). Headless-capable, no UI layer.Packages
A new folder in the repo! The goal for this folder is to ship preconfigured Player entrypoints so consumers don't have to assemble plugins themselves. The
plugins/directory exports building blocks;packages/exports ready-to-use Players for specific content formats.Each preset comes with the A2UI plugin pre-added and appends any consumer-supplied plugins after it. Since
HeadlessPlayer/AndroidPlayerarefinal, the JVM/Android entries are idiomatic factory functions; iOS is a thinViewwrapper that also defaultsstartOptions: .a2ui.React:
@player-ui/a2uiJVM:
packages/a2ui/jvmAndroid:
packages/a2ui/androidiOS:
packages/a2ui/swiftuiMocks
The canonical catalog is
plugins/a2ui/mocks(21 snapshots). JS consumes it via@player-ui/a2ui-plugin-mocks; JVM/Android via//tools/mocks:jar. iOS mirrors the full set as faithful inline copies (the existing iOS demo/test convention, there is no runtime mechanism to read the canonical JSON on iOS today).Storybook
New A2UI stories with one story per asset, driven by a
createA2UIStoryhelper in the Player storybook extension that can render A2UI content.Demo Apps
.a2ui.PlayerViewModelregistersA2UIPluginalongside the reference assets (disjoint type namespaces).Notes
Image/Icon(no Coil /material-icons-coreonly) and iOSImage(deployment target iOS 14 predatesAsyncImage) render labelled placeholders; iOSIconuses SF Symbols.To Align On
Overall Approach
Change Type (required)
Indicate the type of change your pull request is:
patchminormajorN/ADoes your PR have any documentation updates?
Release Notes
TBD
📦 Published PR as canary version:
0.16.0--canary.866.36801Try this version out locally by upgrading relevant packages to 0.16.0--canary.866.36801