Skip to content

Compatibility

MacroDeck.Ui, MacroDeck.Ui.Model, and MacroDeck.Ui.Testing are public NuGet contracts. Existing keys, component meanings, patch semantics, and public API members must follow the normal compatibility policy.

The UI model’s Minimum is 3 and its Current is 4, and the gap between them is the difference between a rename and a widening.

The floor moved to 3 because the component vocabulary was renamed outright rather than extended: every widget.* node type became either ui.* or macrodeck.*, with no alias kept for the old spelling. A tree built against the old vocabulary is not representable under the new one, so a package that no longer understands a single widget.* type must not advertise the majors in which those were the only spelling.

The ceiling moved to 4 because an icon property may now carry a typed {"type":…,"reference":…} provider reference where it previously always carried a bare icon-pack reference string - the same shape as the move to 2, where a text property gained the option of carrying a localization reference. A producer built against 3 emits only the string form and one built against 4 may emit either, so the two are not interchangeable and the version has to say which a session speaks. The floor stayed where it was because nothing was renamed: a reader at 4 accepts both readings, so it still reads every tree a producer at 3 emits, and UiIconInput is unchanged.

Either way a client and a provider negotiate the model version as they always have - before a session is opened - and a mismatch produces the same graceful decline, never a tree the reader cannot parse.

New component types and new properties are additive and leave the model version alone; the component profile’s rule decides which of the two a new feature is. Modifiers add both:

Addition Shape An older reader
modifiers (background, radius, border, accessibility text, disabled) A property on any node Ignores it and draws the node plainer. A disabled subtree still offers none of its own events, because the DSL stopped it declaring them, but the reader does not know the region absorbs the tile’s press, so a deck tile’s own flows still run there.
drag, drag-end, swipe, pinch, pinch-end Event names Never sends a name it does not implement.
pointer-down, pointer-move, pointer-up, tap Event names Never sends them, and does not claim the pointer for a node that declares only these: a deck tile’s own press flow still runs when the node is pressed there.
offersStateProvider, offersIconProvider, stateProviderBlockId, iconProviderBlockId on actions-list-editor, and the provide config event Properties and an event name Ignores the properties, never sends provide, and shows the action list without provider controls.
status (UiStatus), menu (UiConfigMenu) and dialog (UiConfigDialog) Configuration types Declines them and draws the node’s fallback.
confirmTitle, confirmMessage, confirmLabel, confirmDanger, promptValue on button Properties Raises activate at once, without asking and without a payload.
interaction on ui.slider (relative) A property Ignores it and keeps the absolute drag: a press jumps the level to the pointer, and a tap sends change.
ui.modifier (padding, opacity, clip, mask, frame), component version 1 A type Draws the node’s explicit fallback; without one, none of the wrapped content (Macro Deck’s renderer shows a faint placeholder box). No fallback is invented for you.
spans on ui.text A property Ignores it and draws text, which the producer keeps the plain equivalent of the spans: the same words, each image as its alt text, on one font and colour.
overflow on ui.stack (clip-start), component version 2 A property, gated by a component version Draws the node’s fallback, because a producer using clip-start asks for version 2. Without that ask a version 1 reader ignores the key and shrinks every child into the box.
anchor on ui.list (end), component version 3 A property, gated by a component version Draws the node’s fallback, because a producer using end asks for version 3. Without that ask a version 2 reader ignores the key and leaves the view where the user put it, so new rows at the end stay below the fold.
ui.responsive and its variants property, component version 1 A type Draws the node’s fallback. Unlike ui.modifier, one is invented when you set none: a copy of the default layout. When it decides whether the tile’s own press belongs to a control, it walks every layout, not only the one it would have drawn - see Responsive.
ui.first-fit, component version 1 A type Draws the node’s fallback. One is invented when you set none: a copy of the last layout, the one that is always acceptable. When it decides whether the tile’s own press belongs to a control, it walks every layout - see First fit.
macrodeck.video-stream and its stream property, component version 1 A type Draws the node’s fallback, and opens no session. See Video stream.
transparent as the background of ui.stack and ui.button A value Treats it as no background: a stack draws the tile face behind it, a button its accent colour. The tile face stays, so the folder background does not show through.
allowTransparent on color A property Ignores it and offers no Transparent swatch; a stored transparent still shows as the input’s value.

See ADR 0064 for why the vocabulary is organized as a registry over the ui.*/macrodeck.* namespaces rather than one flat list, and The UI model for where negotiation sits in a session’s lifecycle.

These change what an existing plugin observes without a new model version. Each is a deliberate exception to the compatibility policy, listed here so you can check your plugin against it.

Change Hosts What an existing plugin sees
A plugin built on the SDK with the message channel declares the messaging capability kind, at local id provider, whenever the host lists it in capabilityKinds - also when the plugin never uses the channel Released after 3.0.0-beta.11 A test that asserts a plugin’s exact declared capabilities, against MacroDeckTestHost, PluginTestHarness or a real host, sees one more entry. Nothing changes on a host without the kind. See Messaging between plugins.
UiView implements IDisposable, and UiPreviewInstance.DisposeAsync disposes its view - including a UiView a preview scenario returned itself Released after 3.0.0-beta.7 A plugin that never disposes a view behaves as before. A preview scenario that returns a cached UiView gets a disposed view that ignores every event from the second open on; build a new one per call. A plugin that builds with CA1001 as an error may see it on a type that creates a UiView in a field without being disposable. See Disposing.
A link with an external http or https URL is opened by the host in the default browser on every surface, whether or not it declares activate Released after 3.0.0-beta.6 A link that declares activate still receives it exactly once, and is now opened by the host outside the integration setup dialog too. A handler that opened the URL itself opens a second tab; remove that code. See Links.
IUiConfigFlow.CreateUiSessionAsync can be called again for the same flow instance, after its previous session ended with a retryable error or was reloaded for .NET Hot Reload Released after 3.0.0-beta.13 A flow that assumed one session per flow instance gets a second call. Build each session from state the flow holds; the user’s unsaved edits arrive as change events. See From a config flow.
A device provider receives a null DeviceSurfaceAppearance.BackgroundColor for a widget whose stored background is transparent, instead of the literal Released after 3.0.0-beta.14 A provider that drew the stored string sees no colour for that key, like a widget without a background; one that parsed the literal as a colour no longer fails on it. See Devices.
Under .NET Hot Reload, a plugin built on this SDK ends every open session except dialogs with ui/reload, and Macro Deck opens each one again SDK released after 3.0.0-beta.13; on an older host the SDK sends ui/fault instead A provider is asked for new sessions for views that are already open, each time Hot Reload applies a change. A plugin that is not being hot reloaded sees nothing new. See Real views.
PackageSigner and PackageVerifier refuse an icon pack with more than 30,000 archive entries, counting the signature files, with too-many-entries; an icon pack’s pack.json may be up to 32 MiB instead of 8 MiB SDK released after 3.0.0-beta.13 Signing a pack that would exceed 30,000 entries now fails. No host ever imported more than 10,000 entries, so no signed pack that installs is affected. Plugin, profile and template manifests keep the 8 MiB bound. See Publish an icon pack.
An exported icon pack, profile or widget carries one master image per icon and no smaller sizes; Macro Deck creates the sizes it serves from the master when an icon is first shown, and ignores size files in packs it imports Released after 3.0.0-beta.13 An icon pack bundled from a newer export has half to a quarter of the files. Macro Deck 3.0.0-beta.13 and older import it but show its full-size masters at every size. A bundled pack exported by an older version still imports; its size files are not used. See Publish an icon pack.
Turning an integration off withdraws everything its providers registered - widget types, layouts, folder views, screensavers - and takes its devices offline. While it is off, registering one of those is validated and answered as usual but not kept, and its devices stay offline whatever presence is reported Released after 3.0.0-beta.14 A plugin the user turned off no longer has its widget types, layouts, folder views or screensavers offered, including after it reconnects, and its devices show offline. Nothing changes while it is on, or for a plugin that was never configured. When the user turns it back on, the plugin is asked to initialize again, as after a configuration change, and registers what it offers.
A ui.list reader starts its reveal count over when the list’s content is replaced: it holds fewer children than at its previous paint, or the child at the furthest index sent is gone or has another id Released after 3.0.0-beta.11 A reveal can now carry an index at or below one the plugin already received, after its list shrank or its rows were replaced, including a row inserted or removed above the furthest index. A handler that only grows its window, as List shows, is unaffected; one that sets its window from the index unconditionally can shrink it and should keep the larger value. A reader that has not adopted this rule, such as an older Companion app, may not ask again after a replacement.