Skip to content

feat(a2ui_flutter): add the v1.0 examples to the gallery - #3620

Closed
andrewkolos wants to merge 196 commits into
mainfrom
feat/flutter-adapter-v1-gallery
Closed

andrewkolos wants to merge 196 commits into
mainfrom
feat/flutter-adapter-v1-gallery

Conversation

@andrewkolos

@andrewkolos andrewkolos commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Adds the v1.0 basic catalog examples to the gallery from #3094, which runs on the v1.0 adapter from #3619, and a v0.9 / v1.0 selector above the examples list, as the Lit, React and Angular explorers have.

Changes

  • example.dart: each example carries its protocol version. assets/examples/v0_9 and assets/examples/v1_0 link to each version's basic catalog examples.
  • example_run.dart: each run's processor registers the demo catalog and the v1.0 basic catalog, and reads the messages at the example's version.
  • explorer_app.dart: the selector. Switching versions opens the example of the same file name, or the version's first example.
  • The demo catalog stays v0.9, because its custom components are used only by the v0.9 example 37_native-grid.json.

Testing

The test that runs and renders every example now covers the 43 v1.0 examples as well. New widget tests switch to v1.0 and type into a v1.0 TextField bound to the data model, and switch away from an example that v1.0 lacks.

Pre-launch Checklist

One time:

For this PR:

  • I have updated the relevant CHANGELOG.md file.
  • I updated/added relevant documentation.
  • My code changes (if any) have tests.
  • If my branch is on a fork, I have verified that scripts/e2e_test.sh passes.

ℹ️ Initial text drafted by agent

a2ui_explorer is the renderer's gallery app, as section 8 of the
framework adapter blueprint describes. It lists the v0.9 basic catalog
examples, steps through each one's messages with Advance, Run all and
Reset, and shows each surface next to its live data model and the
actions and errors it reports. A demo catalog adds the custom components
the examples name. Its integration tests drive the examples through
static rendering, layout, two-way binding, reactive updates and action
context inside a template.
#3194 renamed A2uiClientError to A2uiRendererError and
MessageProcessor.groupModel to model, and replaced EventSource.addListener
with subscribe, so the gallery no longer analyzed against main.

The demo catalog also passes the basic catalog's definitions, which the
generated component schemas reference as #/$defs/CatalogComponentCommon.
Without them every example failed to resolve its schemas and rendered
nothing.

⚠️ Agent-generated
Same ignore line #3249 added to a2ui_flutter. Pull request CI runs
Flutter stable only, but pushes to main also run beta, where the
material.dart import is reported as deprecated.

⚠️ Agent-generated
copyWith keeps the basic catalog's protocol version, title,
description and instructions, which the hand-built catalog left out.

⚠️ Agent-generated
DataModel.subscribe already skips the current value when it is
subscribed to, so the data model view no longer needs its own flag for
that. ExampleRun.dispose now disposes the whole message processor, not
only its surface group, as its doc says.

⚠️ Agent-generated
…tions

An example name or description with a newline or carriage return put a
raw line break inside a single-quoted literal, which does not compile.

⚠️ Agent-generated
flutter_markdown_plus styles links as tappable and shows a click cursor
over them, but does nothing on a tap without onTapLink. The Markdown
builder now opens the link with url_launcher.

⚠️ Agent-generated
…indow

- Each run gets new Messages, Data Model and Action Log lists, so a
  newly selected example opens at the top instead of keeping the last
  one's scroll offset. Only the lists are keyed by the run, so the
  Reset button keeps keyboard focus.
- CustomGrid's stretched column of Expanded cells threw "BoxConstraints
  forces an infinite width" inside a horizontal List. It now limits
  itself to the screen's width there, as the basic catalog's components
  do.
- The macOS window opens at 1280x800. The template's 800x600 window is
  narrower than the layout's 1120 pixel minimum, so the right column
  opened off-screen.

The Native Grid test now checks the grid's two columns, and the Markdown
test a heading's text.

⚠️ Agent-generated
Each entry now shows the action or error as toJson gives it, so an
action's time and an error's details appear, as in the TypeScript
galleries. New tests cover the errors the processor throws, the errors
a surface reports, and a deleteSurface message.

⚠️ Agent-generated
The label rounded the value, so 0.3 showed as 0, while the data model,
the basic Slider and the TypeScript galleries show 0.3. A move rounded
to a whole number could also land outside fractional bounds, or let a
slider from 0.2 to 0.8 write only its two bounds. A move now writes min
plus a whole number of steps of 1, as far as max allows.

⚠️ Agent-generated
…rately

The gallery README now says the gallery is for protocol v0.9, that the
log also holds the errors the message processor reports, and that the
examples are generated because a Flutter build does not ship assets
declared from outside the app's directory. The comments on the gallery's
processor and on examples_test say each message is checked against the
surface it leaves behind, which is what a2ui_core does, and list what
those checks allow.

⚠️ Agent-generated
scrollUntilVisible ends by jumping the list to the tile without a frame,
so the app tests tapped the tile's old position and missed for 15 of the
44 examples, leaving the first example selected. They now pump before
the tap.

A new test runs every example through ExampleRun and renders its
surfaces, failing on any logged error or exception. It catches a demo
catalog that cannot process the examples, and errors raised while they
build, without opening each example in the app. The generator test also
covers a backslash.

⚠️ Agent-generated
Expanded takes a key itself, so the KeyedSubtree around it added
nothing.

⚠️ Agent-generated
assets/examples is a link to the specification's v0.9 basic catalog
examples, and the app loads them from its asset bundle at startup. This
replaces the generated copy in examples.g.dart and the tool that had to
be rerun whenever an example changed. A test checks that the bundle
holds every specification example.

⚠️ Agent-generated
…queue

SurfaceModel.dispatchError and dispatchWarning now put the report in a
FIFO queue and return once it is queued. The queue hands reports to
listeners in a microtask, never inside the call that found them, so a
listener may change framework state directly.

SurfaceGroupModel owns one queue for its surfaces and exposes onError and
onWarning, which receive every surface's reports after that surface's own
listeners. addSurface moves a surface's undelivered reports into the
group's queue, detachSurface holds them until the surface is added again,
and deleteSurface and SurfaceModel.dispose drop them.

SurfaceModel.onReportsDelivered fires after a delivery that included the
surface's reports. A listener that throws no longer stops later reports.
Its error goes to the current zone's uncaught-error handler, or is printed
in the root zone. An error that no listener receives is printed, and an
unheard warning is logged at FINE.

Tests that read reports right after building a resolver, changing data or
evaluating an expression now wait for the delivery. Disposing a resolver
inside a listener no longer cancels the reports already queued.

⚠️ Agent-generated
MessageProcessor takes onError and onWarning beside onAction, and
subscribes them to the group's events of the same name, so an app can set
one listener for every surface before the first message.

processMessages and processMessagesAsync open a delivery scope around the
synchronous application of each message. Errors and warnings that mounted
resolvers report while a message is applied reach their listeners when
that message ends, before the next message and before an exception from a
later message propagates. A report found inside processMessages is
therefore seen right after it returns, as before the queue, also when the
same payload deletes the surface afterwards. processMessages called from a
listener delivers after that listener returns. Problems in a message
still throw.

The resolver test that read a report right after processMessages no
longer waits for a microtask.

⚠️ Agent-generated
NodeResolver no longer keeps its own queues of errors and warnings, its
fallback microtask or its discard on dispose and on a failed update. It
dispatches each diagnostic to the surface as it finds it, so the
surface's queue delivers it after the resolver call returns and disposing
the resolver drops nothing. The per-resolver deduplication of unknown
types and cycles is unchanged. The note about a deeper unresolved
ChildList is logged where it is found.

The constructor, updates and dispose of a resolver, and every catalog
function call in DataContext, run under a resolution guard. While it is
open, a processMessages that ends a message defers its delivery to a
microtask, so no listener runs inside resolver work or a catalog function.

Tests that pinned the dropped behaviour now expect an unchanged-null
expression error to arrive after disposal, a failed update to deliver
what it found once, and the unresolved-reference warning to be logged
before the root is published.

⚠️ Agent-generated
A2uiSurface builds its resolver as before, and the diagnostics it finds
now reach the surface's and the processor's listeners after the build,
so a listener may call setState directly. While it is mounted, the
widget requests a frame from SurfaceModel.onReportsDelivered, so a host
that defers its setState to a frame callback gets that frame.

error_listener_set_state_test.dart, carried from the superseded fix
branch, covers a processor listener and a surface listener. New tests
cover a deferring host whose surface mounts after the app settles, a
jump through a list that builds and drops surfaces, and a mounted
surface updated and deleted in one payload.

⚠️ Agent-generated
…e README listener

The a2ui_core CHANGELOG lists the breaking changes to error and warning
delivery and the new processor, group and surface events. The
a2ui_flutter CHANGELOG records that listeners may call setState and that
the widget requests a frame after delivery, and that it needs the next
a2ui_core release.

The README Quick Start subscribes one error listener on the processor's
group model next to the action listener, instead of one per surface in
onSurfaceCreated. It passes decoded JSON to processMessages and a version
to getRendererDataModel. The advice to defer a setState caused by an
error listener is replaced by: listeners call setState directly, or
schedule a frame if they defer.

⚠️ Agent-generated
…alone drop

A node's destruction listener that calls processMessages while
NodeResolver.dispose runs no longer delivers inside dispose. Without the
resolution guard around dispose that report arrived before dispose
returned, and no test noticed.

Disposing a standalone surface drops its queued report. The detached
variant could not show this, because a held report is never delivered
either way. The new test observes the drop through the platform log,
which would print an undelivered error.

⚠️ Agent-generated
Six existing tests build a resolver over a surface whose unknown type or
cycle they expect, with no error listener, so each run printed seven
"A2UI error with no listener" lines. They now subscribe a listener that
ignores the reports. The conformance harness does the same where it
builds a resolver only to read the tree.

⚠️ Agent-generated
attachProblemChannel, runInMessageScope and listenersOf are hidden from
the package barrel. Marking them @internal also makes an import of them
through lib/src from another package an analyzer warning.

⚠️ Agent-generated
…iled updates

The processMessages doc and the README say that processMessages called
from a widget's build runs its listeners inside that build, and the doc
says that a listener which deletes a surface or processes more messages
changes what the later messages of the same payload see. The a2ui_core
CHANGELOG records that a resolver update that throws now delivers what
it found and keeps it deduplicated, and that the nested ChildList
warning is logged when found. The processMessages doc and the CHANGELOG
also say that a catalog function returning a signal runs its body when
the result is read, so a processMessages call from that body during a
data write delivers its reports inside the write.

The NodeResolver class doc names data writes among the calls whose
diagnostics are delivered after they return. ComponentContext's doc no
longer says its default reporter dispatches immediately, and the
A2uiSurface doc no longer ties delivery to the surface's own listeners.

⚠️ Agent-generated
Add `expect_reports` and the layer 1 problem reporting actions to
`conformance_schema.json`, and add the delivery timing, channel lifetime,
and lazy evaluation cases (`T1` to `T10`, `X1` to `X4`, and `L1`) in
`conformance/core/problems.yaml`.

⚠️ Agent-generated
Add `problems_conformance_test.dart` to execute `conformance/core/problems.yaml`
against `MessageProcessor`, `SurfaceGroupModel`, `SurfaceModel`, and
`NodeResolver`.

⚠️ Agent-generated
…ir validity

`Catalog.invoke` has checked a function's resolved arguments against its
argument schema since #2995, and the RPC layer added in #3086 shares
that check through `argumentErrors`. The v1.0 validation rules
(`required`, `regex`, `length`, `numeric`, `email`) return a
ValidationResult, so `and`, `or` and `not` over them received an object
json_schema_builder cannot read. It threw "Unknown JSON type", the
binder reported EXPRESSION_ERROR, and a Button gated by such a check
stayed disabled with valid data. The v1.0 basic catalog examples
09_login-form and 32_advanced-form-validator do this. A validation
result bound from the data model, such as `{"valid": false}`, failed the
DynamicBoolean schema check with EXPRESSION_ERROR.

Rule 1 of the v1.0 protocol's "DynamicBoolean and CheckRule Coercion"
says a DynamicBoolean that evaluates to a validation result stands for
its `valid` boolean. `argumentErrors` now does this on a catalog for
v1.0 or later: the arguments match when they do with each
ValidationResult, and each object with a boolean `valid`, replaced by
that boolean, at any depth. Any object with a boolean `valid` counts,
because the protocol lets a validation result carry more properties.

When the arguments fail with every replacement made, they are all
checked again with none made. So an argument whose schema is an object,
and which holds an object with a `valid` key, still matches, unless
another value in the same call needs the replacement. When both checks
fail, the errors reported are those of the check with none made. Only
the check uses the replacement: the function receives each argument as
it was resolved. The v1.0 `and`, `or` and `not` read their operands
through `ValidationResult.validityOf`, so they work either way, and the
v1.0 `formatNumber` and `formatCurrency` now read their DynamicBoolean
`grouping` the same way, since a validation result can now reach it.
Passing plain booleans to functions, as rule 1 also describes, would
need the coercion directed by each argument's schema.

On a v0.9 catalog nothing is replaced: the v0.9 `and`, `or` and `not`
read a `{"valid": ...}` map as an object, so a check that let it stand
for a boolean would pass an operand the function then reads as truthy.
On every catalog a ValidationResult is checked as its JSON object
rather than throwing, so an unsupported one is an argument error.

⚠️ Agent-generated
…talog.dart

The framework adapter blueprint (section 5) has an adapter export its
v1.0 basic catalog from the primary catalog entry point and its v0.9
catalog from a `v0_9` subpath. This frees the primary entry point for
the v1.0 catalog: `package:a2ui_flutter/basic_catalog.dart` moves to
`package:a2ui_flutter/v0_9/basic_catalog.dart`, and its source to
`lib/src/catalog/basic/basic_catalog_v0_9.dart`, so the v1.0 source can
take the plain name as the v1.0 library does. Nothing else changes: the
library exports the same `basicCatalog()`, `BasicComponents`,
`A2uiMarkdown` and `A2uiMarkdownBuilder`.

Section 1 of the blueprint describes such legacy entry points as aliases
that re-export the root runtime. Here the runtime has always been a
library of its own, `package:a2ui_flutter/a2ui_flutter.dart`, which is
version-agnostic, and the catalog another. So the v0.9 path holds only
the catalog, as `basic_catalog.dart` does, and a v0.9 app changes one
import. The tests that exercise the v0.9 catalog import it from the new
path.

To port this onto a tree where `basic_catalog.dart` is already the v1.0
catalog, apply the moves by hand. Git would move that v1.0 library to
the v0.9 path without a conflict, and would move the v1.0 source away
from `lib/src/catalog/basic/basic_catalog.dart`, which conflicts where
`basic_catalog_v0_9.dart` already exists.

BREAKING CHANGE: `package:a2ui_flutter/basic_catalog.dart` no longer
exists until the v1.0 catalog takes its place. Import the v0.9 catalog
from `package:a2ui_flutter/v0_9/basic_catalog.dart`.

⚠️ Agent-generated
`package:a2ui_flutter/basic_catalog.dart` now implements the v1.0 basic
catalog: `basicCatalog()` carries the id, protocol version, functions,
instructions and definitions of a2ui_core's `BasicCatalog.v1_0()`, and
`BasicComponents` pairs each shared widget builder with its v1.0
component schema. This follows the framework adapter blueprint
(section 5): one set of builders, and one catalog per protocol version
with that version's schemas and core functions. This commit adds the
v1.0 catalog and leaves the shared builders unchanged, so at this commit
a v1.0 surface accepts TextField `placeholder`, Slider `steps` and Video
`posterUrl` but ignores them, and its TextField still checks
`validationRegexp`. Each has a commit of its own.

A v1.0 `BasicComponents` exists, rather than reusing the v0.9 one with
v1.0 schemas swapped in, because an app that builds a catalog of its
own from basic components needs them checked against the v1.0 schemas.
The v0.9 TextField schema ends in `unevaluatedProperties: false` and has
no `placeholder`, so a v0.9 component would reject the v1.0 props.

The v1.0 components are not wrapped in the v0.9 `BasicTheme`. v1.0
removed `theme` and `primaryColor`, and a2ui_core rejects a v1.0
`createSurface` that carries a theme, so the components use the app's
Material theme as they find it. The module blueprint's "Theme ignored"
test, which expects such a message to render, therefore does not apply,
and the codebase blueprint records that.

The test harness passes raw JSON to `processMessages`, which routes each
message by its own `version`, so one harness serves both versions, and
takes extra catalogs for mixed surfaces. The examples test renders all
43 v1.0 basic catalog examples, as well as the 44 v0.9 ones. Two of the
v1.0 examples, 09_login-form and 32_advanced-form-validator, need the
a2ui_core fix for validation results in function arguments.
`test/v1_0_test.dart` covers a `createSurface` carrying its components
and data, `@index`, `openUrl` running from a tap and refused from a
binding, a failing check disabling a Button, a boolean `false` check,
and a bound Icon `svgPath`. A custom catalog test renders a component
from its own catalog, named by `catalogId`, beside the basic one.

The README now starts from v1.0:
- The host passes the agent's JSON to `processMessages` and gives the
  processor an `outboundListener`, which receives the
  `rendererFunctionResponse` to each `callRendererFunction` and each
  `callAgentFunction`.
- The host sends `a2uiRendererCapabilities` and `a2uiRendererDataModel`,
  the v1.0 names, and disposes the processor.
- Custom components go in a catalog of their own, with relative
  `common_types.json` references, and an agent without a2ui_core gets
  the catalog's document from `toJson()`.
- Actions run as a user activation, so builders call them from gesture
  handlers.
A Protocol v0.9 section covers the v0.9 import, custom v0.9 catalogs,
and, for a host that serves both versions, the per-version envelopes,
data model key and capabilities key, and the version that
`getRendererDataModel` then needs. The codebase blueprint describes
both entry points, both schema sources and the theme deviation.

BREAKING CHANGE: `package:a2ui_flutter/basic_catalog.dart` returns as
the v1.0 catalog under the same names. v0.9 code that still imports it
compiles, but registers the v1.0 catalog, which its v0.9 surfaces do
not name. Import the v0.9 catalog from
`package:a2ui_flutter/v0_9/basic_catalog.dart`.

⚠️ Agent-generated
…catalog

v1.0 removed `TextField.validationRegexp` in favour of `checks` with the
`regex` function (evolution guide, section 2.2). The v1.0 TextField
schema has no `unevaluatedProperties: false`, so a2ui_core still accepts
the property on a v1.0 surface. Only the builder can ignore it, and the
shared builder enforced it on both versions: a v1.0 TextField carrying
`validationRegexp` showed "Invalid format" for text the agent never
asked to check.

Both TextFields now build through a new private `_build`, which takes
the pattern as a parameter. `buildTextField`, used by the v1.0 catalog,
passes none and checks no pattern, while the TextField's `checks` still
apply. `buildTextFieldV0_9`, used by the v0.9 catalog, passes
`validationRegexp` as before.

⚠️ Agent-generated
v1.0 adds a `placeholder` to TextField. The builder passes it to the
field as `InputDecoration.hintText`, shown while the field is empty.

When a field has a label, Material shows hint text only while the label
floats above the field, and a resting label sits where the hint would
be. TextField requires `label`, so with the label left to rest the
placeholder would stay invisible until the field takes focus. While a
placeholder is set, the label therefore always floats. The test checks
the placeholder's opacity, which a check that it is merely built would
miss.

The shared builder reads `placeholder` on both versions. The v0.9
TextField schema ends in `unevaluatedProperties: false` and has no
`placeholder`, so a2ui_core rejects it there.

⚠️ Agent-generated
v1.0 adds `steps` to Slider, "the number of discrete divisions in the
slider range", an integer of at least 1. With `steps`, the builder sets
the Flutter Slider's `divisions` to it and writes the value on that
grid as `min + range * k / steps`. Computing each value from the range,
rather than adding `k` steps of `range / steps`, keeps more grid values
exact: 10 steps over 0 to 1 give 0.3 rather than 0.30000000000000004.
A value with no fraction is written as an int, as before.

Any number of steps snaps. A large count costs nothing to draw, because
Flutter's Slider leaves out tick marks closer than three tick widths
(`slider.dart`, `adjustedTrackWidth / divisions! >= 3.0 * tickMarkWidth`).
`steps` is ignored when the range is empty, which has nothing to divide,
or too large for a double, which renders the slider disabled.

Without `steps` nothing changes: a range that is a whole number from 2
to 1000 moves in steps of 1 and writes `min + (moved - min).round()`,
and any other range moves continuously. A v0.9 test pins that path for
a fractional `min`, which a shared step formula would turn into float
noise. The shared builder reads `steps` on both versions, but the v0.9
Slider schema ends in `unevaluatedProperties: false` and has no
`steps`, so a2ui_core rejects it there.

⚠️ Agent-generated
v1.0 adds `posterUrl` to Video, an image "to display before the video
plays". The media player shows an `http` or `https` poster in the video
frame until playback starts. The poster covers the frame, whose ratio
is a 16:9 placeholder until the video reports its own, so a poster of
another ratio leaves no bars around it.

Once the current media has been played, the frame shows the video, even
when it is paused at 0:00 or paused before the first position update. A
new URL starts over. Before the first play, the poster gives way to the
video while the position is past 0:00, as after scrubbing, and returns
if the position comes back to 0:00. When the video fails, the poster
gives way to the failure frame, so the error shows in the frame as well
as in the controls. A poster that fails to load counts as none: the
frame stays empty until the video is ready, then shows the video's
first frame, as it does without a poster.

The shared builder reads `posterUrl` on both versions. The v0.9 Video
schema ends in `unevaluatedProperties: false` and has no `posterUrl`, so
a2ui_core rejects it there.

⚠️ Agent-generated
… A2uiMarkdown renders it

The v0.9, v0.9.1 and v1.0 basic catalog implementation guides ask a
renderer that cannot render Text as Markdown to strip common Markdown
markers. v1.0 Text has no heading variants, so models write headings as
`#` lines, and 17 of the 43 v1.0 basic catalog examples do. Before, an
app without an `A2uiMarkdown` showed the `#`, `**` and backticks as
written.

Every Text that no `A2uiMarkdown` renders now shows its text without the
opening heading marker and one quote marker on each line, link and
image syntax (keeping the link text or the image's alt text), bold,
italic, strikethrough and code delimiters, and backslash escapes. That
is a body Text without an `A2uiMarkdown` above it, a caption Text, and a
v0.9 heading Text, the last two of which an `A2uiMarkdown` never
renders. A body Text that is a single heading line also takes the style
of that heading level, so v1.0 headings still look like headings. Level
6 takes the `h5` style, the smallest heading variant v0.9 has.

Choices, since the text comes from the agent:
- Every pattern is linear in the text. None backtracks over more than
  one line, and single `*` or `_` emphasis cannot contain its own
  marker. A test renders lines of about 50,000 characters of unclosed
  `[`, links, emphasis and heading spaces in under five seconds.
  Patterns of the usual shape take tens of seconds on them.
- The text of code spans and backslash escapes is kept as written, so
  `` `**kwargs` `` and `\*` stay literal, while markers around a code
  span, as in `` **`config.yaml`** ``, are removed.
- A one-line heading in a body Text also loses its closing `#`
  sequence, but only when whitespace precedes it, so `# Learn C#` keeps
  its `#`. Other Text keeps a closing sequence.
- List, table and fence structure is not parsed: list markers and table
  pipes stay, and markers inside them, fences included, are stripped
  like any others. A heading line inside multi-line text loses its `#`
  but keeps the body style, since styling one line of a Text needs the
  rich text an `A2uiMarkdown` provides.
- The stripping is best effort. An escaped backtick pair can still read
  as a code span, and a one-line body Text that is a `#` line indented
  four spaces still reads as a heading.

The README describes this next to `A2uiMarkdown`.

⚠️ Agent-generated
In v1.0 a check's condition can return a validation result with a
`severity`. a2ui_core normalizes every check into `isValid`,
`validationErrors` and `validationResults` on `node.props`: only a
failure of `error` severity, which a check without a severity has,
makes `isValid` false and fills `validationErrors`, while
`validationResults` holds every failure, the `warning` and `info` ones
too. The framework adapter blueprint (section 5) says the inputs SHOULD
render failing `warning` and `info` results "as non-blocking hints
styled by severity".

`ComponentProps.validationResults` reads that list, and the docs of
`isValid`, `validationErrors` and the Button builder now say they cover
`error` severity only.
Each basic input other than Button shows the message of the first
failing `warning` or `info` check as a hint, only while it has no error:
TextField and DateTimeInput as their decoration's helper text, and
CheckBox, ChoicePicker and Slider as small text under the control. A
hint does not block, so a Button stays enabled.

Two departures from the blueprint, which the codebase blueprint
records:
- Warnings and info notes share one style rather than one each,
  because Material's color scheme has no warning role.
- A Button shows no hint. Its only place for a message is the Tooltip
  it gets while a check fails with an error, and a Tooltip that came and
  went with a hint would remount the enabled button and drop its focus.
The TypeScript web elements show no hints at all.

No basic validation function produces a hint: the v0.9 ones return
booleans, and the v1.0 ones return results without a severity, which
count as errors. A check bound to a validation result object in the
data model produces one on either version, since a2ui_core does not
gate check normalization by version.

⚠️ Agent-generated
Rule 1 of the v1.0 protocol's "DynamicBoolean and CheckRule Coercion"
says that whenever a DynamicBoolean, such as a boolean component
property, evaluates to a validation result, the renderer uses its
`valid` boolean. a2ui_core resolves the property to the result itself,
and `ComponentProps.asBoolean` read anything but a `bool` as null. In
the v1.0 basic catalog, a CheckBox `value` bound to a validation rule,
such as `{"@call": "required", ...}`, therefore read as unchecked.

`asBoolean`, and so `props.boolean`, now reads a ValidationResult, or a
map with a `valid` key, by its validity through
`ValidationResult.validityOf`, the rule a2ui_core's v1.0 `and`, `or`
and `not` use. Any other value that is not a `bool` still reads as
null.

The rule applies on both versions. `asBoolean` is static and has no
catalog version to gate on, and v0.9 gives an object in a boolean
property no meaning, so reading one by its `valid` changes only payloads
whose v0.9 meaning is undefined. A v0.9 boolean property holding a
`{"valid": ...}` object read as null before. This differs from the v0.9
`and`, `or` and `not`, whose argument check rejects such an object.

The test relies on the `hide ValidationResult` import that the hints
commit added to `component_props_test.dart`, so a port that takes this
commit without that one needs the import as well.

⚠️ Agent-generated
v1.0 adds `live` ("off", "polite" or "assertive") and `hidden` (a
DynamicBoolean) to a component's `accessibility`. The framework adapter
blueprint (section 3) asks for the attributes to be mapped in one place
per adapter, such as the node dispatcher, so custom components get them
too. NodeView already put `label` and `description` on a Semantics node
around each resolved component, and now maps the other two there:
- `polite` and `assertive` make that node a live region. Flutter's
  `Semantics.liveRegion` is a single flag with no assertive level, so
  both announce politely. `off`, or no `live`, sets no flag at all,
  which leaves the semantics of every other component as it was.
- A `hidden` that reads as true excludes the component and its whole
  subtree from semantics, as `aria-hidden` does. A bound `hidden`
  follows the data through the a2ui_core binding fix for
  `accessibility.hidden`, and one set by a validation rule reads by its
  validity through `ComponentProps.asBoolean`.
The semantics widgets stay in the tree whatever the attributes are, so
changing one keeps the subtree's state.

The mapping applies on both versions. v0.9 defines neither attribute,
but a v0.9 payload that writes one as a literal gets it. A bound
`hidden` on a v0.9 basic component is not resolved, since the v0.9
ComponentCommon schema has no `hidden`.

Known limits:
- A live region announces changes to its own label. On a TextField the
  region is the control's own node, so changes to its label are
  announced, but not its typed value. On a Slider the region is a parent
  node that takes in the label and value text above the control, and on
  a CheckBox, ChoicePicker or Slider it takes in the check message below
  the control, so changes to these are announced. A Button's child text
  and a CheckBox's label sit in the control's own node below the region,
  so their changes are not announced. The README says this.
- An explicit `accessibility.label` does not replace the label a
  control takes from its own text, which the v1.0 protocol asks for. On
  a Button, CheckBox or Slider, `label`, `description` and `live` land
  on a parent node of the focusable control, and on a TextField or Text
  the explicit label is joined to the widget's own label. This predates
  v1.0 support, and replacing a label needs the controls to place it
  themselves. The README and the codebase blueprint record it.
- An open Modal's content is drawn in the Overlay, outside the Modal's
  subtree, so a `hidden` Modal hides its trigger but not its open
  content.

With this commit the adapter covers the v1.0 sections of the framework
adapter blueprint, apart from the deviations the codebase blueprint
records, so the codebase blueprint now pins the blueprint's current
commit, b1dd650: the v1.0 alignment of b161f9b plus later edits that
only change paths.

⚠️ Agent-generated
…booleans in one pass

`argumentErrors` tried the schema with each validation result swapped for
its validity and, when that failed, again with none swapped, for a function
whose argument schema takes an object that holds `valid`. No v1.0 function
takes one. On v1.0 the single swapped pass now stands, and its errors are
the errors.

⚠️ Agent-generated
…arker list alone

The fallback for a Text with no `A2uiMarkdown` protected code spans and
backslash escapes behind sentinels and trimmed a heading's closing `#` run.
Code delimiters and escape backslashes are now two more entries in the
marker list, so a marker inside a code span is stripped too, and a closing
`#` run stays in the heading text.

⚠️ Agent-generated
…ject

`argumentErrors` on a v1.0 catalog swapped every `ValidationResult`, and
every object with a boolean `valid`, for that boolean before the schema
check, so a function taking an `object` argument that held `valid`, or a
`ValidationResult` itself, failed the check with EXPRESSION_ERROR, "Value
`true` is not of type `object`".

The check now leaves arguments as their JSON and instead widens the
function's resolved argument schema, once per catalog and function, so
that on v1.0 or later each boolean position also accepts an object with a
boolean `valid`. A validation result still passes where a DynamicBoolean
coerces it, and passes as itself where an object is expected.

⚠️ Agent-generated
A Text that no A2uiMarkdown renders removed backslash escapes and code
span backticks before the emphasis markers, so the text they protect was
then stripped as markers: `\*not italic\*` showed as "not italic", and
`__init__` in backticks as "init". Escaped characters and code span text
are now hidden from the marker patterns as private-use characters and
restored afterwards, so they show as written.

A heading's closing sequence of #s, as in `## Heading ##`, showed after
its text. A pattern that stays linear in the text now drops it, in a
one-line Text and in the heading lines of a longer one.

⚠️ Agent-generated
The snapped value was bounded by `max` alone, in each of the two stepping
branches. A single `clamp(min, max)` on the stepped value says the same
thing once, and holds the lower bound too, although a move cannot cross
it: Flutter's Slider hands `onChanged` a value from `min` to `max`, and
each branch adds a non-negative amount to `min`.

⚠️ Agent-generated
@andrewkolos
andrewkolos removed this pull request from stack #3644 October 27, 2026 17:38
@andrewkolos
andrewkolos added this pull request to stack #3659 October 27, 2026 17:39
@andrewkolos
andrewkolos removed this pull request from stack #3659 October 27, 2026 18:51
@andrewkolos
andrewkolos force-pushed the feat/flutter-adapter-v1-example branch from c7bceef to 50303e1 Compare October 27, 2026 18:51
@jacobsimionato

Copy link
Copy Markdown
Collaborator

The diff here looks bigger than expected! I might wait for you to have a chance to merge some of the underlying PRs then take another look!

Base automatically changed from feat/flutter-adapter-v1-example to main October 29, 2026 19:19
@andrewkolos

Copy link
Copy Markdown
Collaborator Author

Closing this as superseded by #3094. Once #3619 landed I rebuilt the example PRs on main instead of stacking them, and folded this PR's change into #3094

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants