Serving a configuration view
Serve a tree for an integration’s config flow or one configured action instance, beside the declared fields it never replaces.
Example
Section titled “Example”A config flow whose step is also drawn as a tree. The input keys match the declared field names:
public sealed class MediaServerIntegration : IPluginIntegration, IUiConfigFlowProvider{ public IReadOnlyList<IActionDefinition> Actions { get; } = [new ToggleAction()];
public IConfigFlow CreateConfigFlow() => new MediaServerConfigFlow();
public Task InitializeAsync(IIntegrationContext context) => Task.CompletedTask;
public Task ShutdownAsync() => Task.CompletedTask;}
public sealed class MediaServerConfigFlow : IUiConfigFlow{ private static ConfigFlowStep ConnectionStep() => new() { StepId = "connection", Title = Strings.Setup.ConnectionTitle(), Fields = [ ActionParameter.Secret("api_key", label: Strings.Setup.ApiKey(), required: true), ActionParameter.Number("poll_seconds", label: Strings.Setup.PollInterval(), min: 5, max: 3600, defaultValue: 30), ], };
public Task<ConfigFlowResult> StartAsync(IConfigFlowContext context, CancellationToken cancellationToken) => Task.FromResult(ConfigFlowResult.Step(ConnectionStep()));
public async Task<ConfigFlowResult> SubmitAsync(string stepId, IReadOnlyDictionary<string, object?> input, IConfigFlowContext context, CancellationToken cancellationToken) { var apiKey = input.GetValueOrDefault("api_key") as string ?? string.Empty;
return await MediaServerClient.CanConnectAsync(apiKey, cancellationToken) ? ConfigFlowResult.Complete("Media server") : ConfigFlowResult.Error(ConnectionStep(), Strings.Setup.CannotConnect()); }
public Task<IUiSession?> CreateUiSessionAsync(UiSessionRequest request, CancellationToken cancellationToken) { var apiKey = new UiState<string>(string.Empty); var pollSeconds = new UiState<double>(30);
var root = new UiConfigStack { Key = "root", Children = [ new UiSecretInput { Key = "api_key", Label = Strings.Setup.ApiKey(), Required = true, Binding = Bind.To(apiKey), }, new UiNumberInput { Key = "poll_seconds", Label = Strings.Setup.PollInterval(), Description = Strings.Setup.PollIntervalHint(), Min = 5, Max = 3600, ShowSlider = true, Binding = Bind.To(pollSeconds), }, ], };
return Task.FromResult<IUiSession?>(new ViewSession(new UiView(request.Surface, root))); }}ViewSession is the adapter from Serving a view.
The tree renders a transaction it does not own
Section titled “The tree renders a transaction it does not own”// Declared fields stay - they are the fallback and they mark secrets.ActionParameter.Secret("api_key", label: Strings.Setup.ApiKey(), required: true)
// The tree's top-level input with the same key feeds the same submit.new UiSecretInput { Key = "api_key", Label = Strings.Setup.ApiKey(), Binding = Bind.To(apiKey) }A configuration tree never completes a flow and never persists a parameter. SubmitAsync stays the only
way a flow accepts values, and the ordinary save path the only way an action instance is written - which
keeps secret encryption, OAuth and entry replacement unchanged
(ADR 0050).
- Keep serving the declared fields.
ConfigFlowStep.FieldsandIActionDefinition.Parametersare still required: they are the fallback, and for a config flow they tell Macro Deck which submitted values are secret. - Name top-level inputs after their fields. A top-level input’s node id is the field key it submits.
For a config flow, Macro Deck draws the dialog’s Continue and Cancel buttons itself, so the tree needs no submit control of its own:
- Continue submits what the tree shows. When the user presses it, the current value of each input whose
id is a declared field name is sent to
SubmitAsync, including a value the user typed and one you patched in. Inputs whose id is not a declared field name are not picked up this way. UiFlow.CanSubmitgates Continue when you set it. Leave it unset and Continue is enabled once every visible required declared field has a value in the tree. Either way, aConfigFlowResult.ErrorfromSubmitAsyncis shown in the dialog.- The tree is not seeded from the stored entry. On a reconfigure your flow is not handed the existing values, so the tree shows - and Continue submits - whatever you built it with. Seed it from your own live configuration if a reconfigure should start from the current values.
From a config flow
Section titled “From a config flow”public sealed class MediaServerIntegration : IPluginIntegration, IUiConfigFlowProvider { /* ... */ }
public sealed class MediaServerConfigFlow : IUiConfigFlow{ public Task<IUiSession?> CreateUiSessionAsync(UiSessionRequest request, CancellationToken cancellationToken) => Task.FromResult<IUiSession?>(new ViewSession(new UiView(request.Surface, BuildTree())));}IUiConfigFlowProvider goes on the integration; describe then reports servesConfigUiTree.
IUiConfigFlow goes on the flow object itself, so the tree’s state and SubmitAsync’s state are one
thing. Macro Deck relays the tree without inspecting it, so it cannot tell which tree values are secret:
a flow serving a tree classifies its own secrets by returning them as ConfigFlowValue.Secret from
Complete.
CreateUiSessionAsync can be called more than once for the same flow: Macro Deck opens a new session after
a session ended with a retryable error, and after .NET Hot Reload updated your plugin. Build each session
from state the flow holds, not from anything the previous session kept. The user’s unsaved edits reach the
new session as change events, the same way they reached the first one.
From an action
Section titled “From an action”public sealed class ToggleAction : IUiConfigurableActionDefinition{ public string Id => "toggle";
public LocalizedText Name => Strings.Toggle.Name();
public LocalizedText Description => Strings.Toggle.Description();
public IReadOnlyList<ActionParameter> Parameters { get; } = [ ActionParameter.Text("target", label: Strings.Toggle.Target(), required: true), ActionParameter.Text("mode", label: Strings.Toggle.Mode(), defaultValue: "toggle"), ];
public IActionExecutor CreateExecutor() => new ToggleExecutor();
public Task<IUiSession?> CreateConfigurationSessionAsync(ActionConfigurationRequest request, CancellationToken cancellationToken) { var target = new UiState<string>(Read(request, "target") ?? string.Empty); var mode = new UiState<string>(Read(request, "mode") ?? "toggle");
var root = new UiConfigStack { Key = "root", Children = [ new UiStringInput { Key = "target", Label = Strings.Toggle.Target(), Required = true, Binding = Bind.To(target) }, new UiChoiceInput { Key = "mode", Label = Strings.Toggle.Mode(), Segmented = true, Binding = Bind.To(mode), Options = UiValue.Of<IReadOnlyList<UiOption>>([ UiOption.Of("on", Strings.Toggle.ModeOn()), UiOption.Of("off", Strings.Toggle.ModeOff()), UiOption.Of("toggle", Strings.Toggle.ModeToggle()), ]), }, ], };
return Task.FromResult<IUiSession?>(new ViewSession(new UiView(request.Session.Surface, root))); }
private static string? Read(ActionConfigurationRequest request, string name) => request.Parameters.TryGetValue(name, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString() : null;}One session is created per open configuration surface, so several can be live for the same action at
once - keep state on the session, never on the definition. request.Parameters holds the instance’s
stored values, keyed by parameter name. Secret and Password parameters arrive masked
(UiConfigSurfaceAttributes.MaskedSecretValue, "$masked"), because opening a configuration surface is
not an intent to reveal a secret. You get the real value the ordinary way, when the user submits.
Reporting values the tree edits
Section titled “Reporting values the tree edits”Macro Deck saves what the tree’s top-level inputs report as change events, keyed by their ids. A value
the tree builds itself - nested fields, a list of rows, a value computed from several inputs - has no
top-level input to report it. Wrap the session in an ActionConfigurationSession and report the
parameter yourself:
public Task<IUiSession?> CreateConfigurationSessionAsync(ActionConfigurationRequest request, CancellationToken cancellationToken){ var rows = new UiState<IReadOnlyList<Row>>(ReadRows(request)); ActionConfigurationSession? session = null;
var root = BuildEditor(rows, onEdited: () => session!.Report("rows", JsonSerializer.SerializeToElement(ToStoredMap(rows.Value))));
session = new ActionConfigurationSession(new ViewSession(new UiView(request.Session.Surface, root))); return Task.FromResult<IUiSession?>(session);}- A report is a user edit of that parameter. The value goes into the action being edited exactly as if the user had typed it into the parameter list, and like any edit it is saved only when the user saves.
- Only declared parameters. A name that is not in
Parameters, aSecretorPasswordparameter, and a value whose JSON shape does not fit the parameter type (aKeyValuemust be an object of strings, anArrayan array) are dropped - by the host and again by the client. - Report from the event handler. Before it saves, the desktop app waits until every event it already
sent has been handled and every value reported while handling it has arrived. That covers a report
raised synchronously inside
Dispatch. A report raised after awaited work, a lookup or a timer, can arrive after the user saved and is then lost until the next edit. - Older hosts and clients ignore reports. The session still works as a plain session there: the tree shows and edits, but a value only reported is not saved. Keep a top-level input, or the declared parameter list, as the way to set anything that must be saved everywhere.
IActionConfigurationSessionis the interface behind the wrapper, for a session that raisesParameterValuesReporteditself.
Letting the user search a long list
Section titled “Letting the user search a long list”A choice draws a plain select. When its list is long, a few dozen counters, scenes or devices, set
Searchable so the user can type into the field to find an option:
new UiChoiceInput{ Key = "counterId", Label = Strings.Widget.Counter(), Searchable = true, Binding = Bind.To(counter), Options = UiValue.Of<IReadOnlyList<UiOption>>(counters),}- Typing filters the options by label, and the value is still one of the options: typed text alone is never stored, and the field shows the picked option’s label, not its value.
UiDynamicChoiceInputtakes the sameSearchable, and searches the options it resolved.- On a choice that is also
Segmented,CardsorAllowsCustomValue,Searchablehas no effect. A choice that allows a custom value is already a field that filters as the user types. - A Macro Deck release from before
Searchableignores the property and draws the plain select.
For an action parameter, see Action parameters.
Letting the user pick a file, folder or image
Section titled “Letting the user pick a file, folder or image”For a path, use a path input rather than a UiStringInput, so the user can browse instead of typing:
var folder = new UiState<string>(string.Empty);var placeholder = new UiState<string>(string.Empty);
new UiConfigStack{ Key = "root", Children = [ new UiFolderInput { Key = "folder", Label = Strings.Frame.Folder(), Required = true, Binding = Bind.To(folder) }, new UiImageInput { Key = "placeholder", Label = Strings.Frame.Placeholder(), Description = Strings.Frame.PlaceholderHint(), FileExtensions = UiValue.Of<IReadOnlyList<string>>(["png", "jpg", "webp"]), Binding = Bind.To(placeholder), }, ],}| Node type | DSL element | Value |
|---|---|---|
folder |
UiFolderInput |
A folder path |
file |
UiFileInput |
A file path, optionally limited to FileExtensions |
image |
UiImageInput |
An image file path, limited to image formats unless FileExtensions says otherwise |
The value is a plain path string on the computer that runs Macro Deck, not on the device the user is looking from. The path inputs work in every configuration surface on this page, including widget, folder view and screensaver configuration.
FileExtensionsare bare extensions, without the dot:["png", "jpg"], not[".png"]. They narrow what browsing and dropping offer. Leave them unset and a file input accepts any file, while an image input offers the image formats the renderer can draw (PNG, JPEG, GIF, WebP, SVG). Setting them on an image input replaces that list rather than adding to it.- Macro Deck does not check the path. The user can type or paste any value, including one outside
FileExtensions, and a file can be moved or deleted after it was picked. Handle a missing file where you read it. Label,DescriptionandPlaceholderapply as on any input, andRequiredmarks the label. Without aPlaceholder, the renderer shows its own hint for the kind of path.
The Macro Deck desktop editor draws each as a text field with a Browse button. In the desktop app, Browse opens the operating system’s file or folder dialog; where that is not available, it opens Macro Deck’s own file browser. A file or folder dropped onto the field fills it in, if it matches the input. The image input shows no preview of the picked file.
In a declared field list, the counterparts are ActionParameter.File (with fileExtensions),
ActionParameter.Folder and ActionParameter.Image, which takes no extensions and always offers the
image formats.
Editing code
Section titled “Editing code”A UiCodeInput draws Macro Deck’s code editor, with line numbers, undo, search and bracket matching:
new UiCodeInput{ Key = "script", Label = Strings.Script.Label(), Language = UiCodeLanguages.AutoHotkey, VariableInsertFormat = "MacroDeck.GetVar(\"{name}\")", Binding = Bind.To(script),}| Node type | DSL element | Value |
|---|---|---|
code |
UiCodeInput |
The source text, or nothing in content-resource mode (below) |
Languagepicks the highlighting.UiCodeLanguagesnames the languages Macro Deck ships highlighting for:text,json,liquidandautohotkey(v2). The vocabulary is open: a language a client does not know is edited as plain text, never refused.ActionParameter.Codetakes the same values.VariableInsertFormatadds an Insert variable button. The editor inserts the format at the cursor with every{name}replaced by the picked variable’s name; names are letters, digits and_ . : -, so nothing is escaped. Without it there is no button, and a client from before the property ignores it.Rowssets how many lines the editor shows, which is its visible height; longer text scrolls. Without it the editor keeps its default height. The desktop app clamps the value to 3 to 60 lines, and a client from before the property draws its default height.
Text too large for the tree
Section titled “Text too large for the tree”Up to about a megabyte of source does not belong in a tree, which every patch would carry. Register the
text as a text/plain UI resource and hand the
handle to ContentResource; the editor loads the text from it instead of the bound value:
var name = "script-" + Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(16));var handle = await context.UiResources.RegisterAsync(name, Encoding.UTF8.GetBytes(source), "text/plain");
new UiCodeInput{ Key = "script", Language = UiCodeLanguages.AutoHotkey, ContentResource = UiValue.Of(handle), Events = [ UiCodeInput.OnContentUploaded(async (upload, cancellationToken) => { var origin = _origin; _draft = await origin!.ReadUploadAsync(upload.UploadId, cancellationToken); }), UiCodeInput.OnContentUploadFailed(() => { }), ],}_origin is the origin your session keeps in Dispatch, as Serving a view shows.
In this mode the text never travels in an event and the input sends no change:
- While the user edits, the editor uploads the whole text (UTF-8, at most 2 MiB) to Macro Deck, shortly after typing stops and when the editor loses focus.
- It then raises
content-uploadedwithuploadId,lengthandcontentHash, the SHA-256 Macro Deck computed in thesha256:<hex>form a resource’s content hash takes.OnContentUploadedhands you that as aUiContentUpload. - Read the bytes with
IUiEventOrigin.ReadUploadAsync(uploadId)on the origin of that event. An origin reads only uploads of its own session. An upload is held for five minutes, until a newer upload of the same input replaces it, or until the session closes; after that the read throwsUiUploadNotFoundException, and you ignore that event, because a newer one follows. - The client holds the session’s later events until the upload event is sent, so a Save button pressed right after typing always sees the latest text.
When the upload fails (too large, the network, more than four uploads pending in the session) the client
shows the failure, raises content-upload-failed without data, and your handler keeps the previous draft.
To save, register the uploaded bytes unchanged as the input’s next resource. When the tree then names a resource whose content hash equals the client’s own last upload, the client keeps its editor state, cursor and undo history included. Changing the bytes, even only a byte order mark or line endings, makes it reload the text.
- Security: a resource is readable by any client signed in to Macro Deck that knows its id, and the id is made of your plugin’s id and the name. Use an unguessable name, as above, register again under a new name rather than reusing one that was shown before, and remove a name when nothing shows it.
- Use it on a page. The desktop app uploads edits only from a page session. In any
other session, such as a config flow, a folder view or a modal, it raises
content-upload-failedinstead, so the provider never receives the edited text there. - A client from before content resources ignores
ContentResourceand edits the bound value, which is empty when you keep the text out of the tree.ReadUploadAsyncon a Macro Deck from before uploads throwsNotSupportedException. - In tests,
FakeUiEventOrigin.AddUploadmakes bytes readable under an upload id.
Letting the user set colour thresholds
Section titled “Letting the user set colour thresholds”A UiThresholdsInput works in a config flow and an action’s configuration as it does in a
widget configuration: a bar of coloured bands with draggable
boundaries, whose value arrives in your binding as a UiThresholds.
Showing what governs a setting
Section titled “Showing what governs a setting”UiStatus is a compact, framed line for “this setting is currently controlled by something else”: an
optional leading Icon, a muted Label and the emphasized Value it introduces. Its children are drawn
trailing the text; an icon-only UiConfigButton there becomes a borderless control, typically the one that
stops what the line names.
var stop = new UiConfigButton{ Key = "stop", Label = "Stop using", Icon = "x", Events = [UiEventHandler.On(UiConfigEvents.Activate, StopUsingProvider)],};
new UiStatus{ Key = "provider", Icon = "zap", Label = "Provided by", Value = "Mute / Unmute", Children = [stop], Fallback = new UiConfigStack { Key = "provider-fallback", Children = [new UiProse { Key = "provider-text", Text = "Provided by Mute / Unmute" }, stop with { Key = "stop-text", Icon = default }], },}A renderer that predates status declines it like any unknown type and draws the Fallback, so give it one
built from a UiProse and the same button where the line matters.
Styled text and cards
Section titled “Styled text and cards”UiProse can carry Spans, the same text spans a ui.text draws,
in place of its plain Text, and a Variant that sets the paragraph as a block: quote, code-block,
heading-1, heading-2, heading-3 or subtext. A paragraph with spans or a variant keeps the line
breaks in its text. Keep Text the plain equivalent of the spans: a renderer that does not know spans
draws Text, and one that does not know a variant draws a plain paragraph. An image span in prose is
drawn as its Alt text.
A UiConfigStack with an Accent colour (#rrggbb) is drawn as a card: a leading bar in that colour on
a fixed dark surface with light text, the same in the light and the dark theme, so text styled for that
surface keeps its contrast. A renderer that does not know accent draws a plain stack.
new UiConfigStack{ Key = "preview", Accent = "#5865f2", Children = [ new UiProse { Key = "message", Text = "Build finished\nnpm ci", Spans = UiValue.Of<IReadOnlyList<UiTextSpan>>( [ UiTextSpan.FromText("Build ", weight: UiComponentTextWeights.Bold), UiTextSpan.FromText("finished") with { Italic = true }, UiTextSpan.FromText("\n"), UiTextSpan.FromText("npm ci") with { Monospace = true, Background = "#1e1f22" }, ]), }, new UiProse { Key = "footer", Text = "Sent by Macro Deck", Variant = "subtext" }, ],}Asking before a button acts
Section titled “Asking before a button acts”A UiConfigButton can ask first. With ConfirmMessage set, the renderer shows a dialog (ConfirmTitle,
ConfirmLabel, and ConfirmDanger for a destructive action) and raises activate only when the user
accepts. With PromptValue set, the dialog asks for text instead, starting from that value with
Placeholder as its hint, and activate carries the entered text as a string payload:
new UiConfigButton{ Key = "rename", Label = "Rename", ConfirmLabel = "Save", PromptValue = UiValue.From(() => current.Value), Events = [UiEventHandler.On(UiConfigEvents.Activate, data => { if (data.TryGetString(out var name) && name.Trim().Length > 0) { current.Value = name.Trim(); } })],}A renderer that predates these properties raises activate at once and without a payload, so a handler
has to tolerate a missing answer, and an action that cannot be undone should not rely on the question alone.
Grouping actions in a menu
Section titled “Grouping actions in a menu”UiConfigMenu is a compact trigger (Icon, with Label as its accessible name) that opens a list of its
child UiConfigButtons, each drawn with its icon and label and each asking first if it declares a question.
A renderer that predates menu declines it and draws the node’s Fallback.
Putting a question to the user
Section titled “Putting a question to the user”UiConfigDialog is a modal dialog shown for as long as the node is in the tree, so a provider opens it by
adding it inside a UiWhen and closes it by removing it. Title is the heading and Text the message; child
UiConfigButtons are the answers, drawn in order with the last one as the primary answer (or as a warning
when it declares ConfirmDanger), and other children such as boolean inputs are the dialog’s body. Closing the
dialog without answering raises cancel on it:
new UiWhen{ Key = "stop-when", Condition = () => asking.Value, Content = () => new UiConfigDialog { Key = "stop", Title = "Stop using this provider?", Text = "Your own states come back.", Events = [UiEventHandler.On(UiConfigEvents.Cancel, () => asking.Value = false)], Children = [ new UiConfigButton { Key = "keep", Label = "Cancel", Events = [UiEventHandler.On(UiConfigEvents.Activate, () => asking.Value = false)] }, new UiConfigButton { Key = "stop-now", Label = "Stop using", ConfirmDanger = true, Events = [UiEventHandler.On(UiConfigEvents.Activate, Stop)] }, ], },}A renderer that predates dialog declines it and draws its fallback in place.
Page layout
Section titled “Page layout”A tree that fills a whole page, such as an integration page, is laid out like Macro Deck’s own library pages with four layout types and a few presentation properties. They work in every configuration surface, but they are made for a page.
| Node type | DSL element | What it draws |
|---|---|---|
split |
UiSplit |
A titled rail beside a detail card; stacked on a narrow screen |
list |
UiConfigList |
Selectable rows, each a list-item |
list-item |
UiConfigListItem |
One row: label, description, icon, badge, status dot, trailing controls |
empty-state |
UiEmptyState |
An icon, a title and a line of text for a region with nothing in it |
UiSplit has three regions: RailActions (controls in the rail’s header beside RailTitle, such as an
icon button that adds an item), Rail (typically a UiConfigList) and Detail (drawn on a card). On the
wire a split node always has exactly three stack children in that order, keyed rail-actions, rail and
detail, each present even when empty, so a reader finds a region by its position and a patch edits a region
like any stack. A region does not open an input scope: an input in the detail keeps its bare key.
UiConfigListItem carries Label, Description (a muted second line), Icon, Badge (a short tag
such as a count), Severity (a status dot: info, success, warning or error, as on a banner) and
Selected. Choosing the row raises activate; set Selected in the handler. Its children are drawn
trailing the row, typically a UiConfigMenu with the row’s own actions, and choosing one of those does not
activate the row. A list contributes no key and no value: the selection is the provider’s own state.
Selection is per session. A page is an exclusive surface, so every client that opens it has its own session, and one client’s selection never shows up in another’s tree. In a shared surface every attached client sees the same row selected.
UiEmptyState carries Icon, Title and Text; its children, such as a button that creates the
first item, are drawn below the text.
The presentation properties change how existing types are drawn:
| Property | On | Values | Absent |
|---|---|---|---|
Variant |
UiConfigButton |
UiButtonVariants: primary, secondary, danger, ghost |
secondary, as before |
IconWithLabel |
UiConfigButton |
true draws the Label beside the Icon |
icon-only when Icon is set, as before |
Align |
UiConfigStack |
UiStackAlignments: start, center, end, space-between |
start, as before |
Gap |
UiConfigStack |
UiStackGaps: none, small, medium, large |
medium, as before |
Variant |
UiConfigStack |
UiStackVariants.Card (card): a framed section on the theme’s card surface |
a plain stack |
Badge |
UiHeading |
A short tag beside the caption, such as “Unsaved” | the caption alone |
Accent keeps its meaning on a plain stack (the fixed dark card described under
Styled text and cards). On a stack whose Variant is card, it adds a leading bar
in its colour to the theme card instead. ConfirmDanger styles a button’s confirmation dialog; use
Variant = "danger" to style the button itself. In a menu, either one draws the entry as a danger entry.
new UiConfigStack{ Key = "footer", Direction = "horizontal", Align = UiStackAlignments.End, Gap = UiStackGaps.Small, Children = [ new UiConfigButton { Key = "discard", Label = Strings.Page.Discard() }, new UiConfigButton { Key = "save", Label = Strings.Page.Save(), Icon = "check", IconWithLabel = true, Variant = UiButtonVariants.Primary, }, ],}No fallback is invented. Like status, menu and dialog, a reader that predates split, list,
list-item or empty-state declines the node and draws its explicit Fallback, or nothing without one. A
reader that predates the properties ignores them and draws an ordinary button, a plain stack and a heading
without a badge. Give a node a Fallback where an older reader matters; Macro Deck’s own desktop app always
draws the current types. See UI model compatibility
and the full example in Integration pages.
Declining and fallback
Section titled “Declining and fallback”public Task<IUiSession?> CreateUiSessionAsync(UiSessionRequest request, CancellationToken cancellationToken) => Task.FromResult<IUiSession?>(null); // the client renders the declared fields| What happens | What the user gets |
|---|---|
You return null |
The declared fields. Not an error. |
| The provider times out, disconnects or trips a session limit | The declared fields, never an error. |
| The client cannot render your UI model version | The declared fields. Negotiated before any session opens - no session, no slot. |
A client renders one representation, never a mix of both.
Entry points
Section titled “Entry points”A config surface names its entry point in UiConfigSurfaceAttributes.EntryPoint:
UiConfigEntryPoints |
Attributes | Served by |
|---|---|---|
IntegrationConfig (integration-config) |
IntegrationId, ConfigFlowSessionId |
IUiConfigFlow.CreateUiSessionAsync |
ActionConfig (action-config) |
ActionId, Parameters |
IUiConfigurableActionDefinition.CreateConfigurationSessionAsync |
FolderViewConfig (folder-view-config) |
FolderId, FolderViewId, FolderViewConfiguration |
Your IUiProvider - see Folder views |
ScreenSaverConfig (screensaver-config) |
DeviceId, ScreenSaverId, ScreenSaverConfiguration |
Your IUiProvider - see Screensavers |
WidgetConfig (widget-config) |
WidgetId, WidgetType, WidgetData, WidgetWidth, WidgetHeight |
Your IUiProvider - see Configuring a widget |
Out of process, MacroDeck.Plugin.Hosting routes integration-config and action-config to the flow or
action first; if that target does not exist, serves no tree or declines, it falls through to your
IUiProviders. Folder views and widgets have no declared field list, so their rules differ - see their
pages.