// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.DependencyInjection;
namespace Aspire.Hosting.Ats;
#pragma warning disable ASPIREINTERACTION001 // IInteractionService and related types are experimental.
/// <summary>
/// ATS exports for the interaction service.
/// </summary>
/// <remarks>
/// <para>
/// The interaction service surface is tailored for polyglot app hosts rather than exposed directly. The shipped
/// .NET API models inputs with delegate-bearing option types (for example <see cref="InputLoadOptions.LoadCallback"/>)
/// that cannot be serialized as ATS DTOs. Instead, polyglot callers build inputs through factory capabilities that
/// return the opaque <see cref="InteractionInputBuilder"/> handle, attach behavior such as dynamic loading via
/// callbacks on that handle, and then pass the handles to the prompt capabilities.
/// </para>
/// </remarks>
internal static class InteractionExports
{
/// <summary>
/// Gets the interaction service from the service provider.
/// </summary>
/// <param name="serviceProvider">The service provider handle.</param>
/// <returns>An interaction service handle.</returns>
[AspireExport]
public static IInteractionService GetInteractionService(this IServiceProvider serviceProvider)
{
ArgumentNullException.ThrowIfNull(serviceProvider);
return serviceProvider.GetRequiredService<IInteractionService>();
}
/// <summary>
/// Gets a value indicating whether the interaction service is available to prompt the user.
/// </summary>
/// <param name="interactionService">The interaction service handle.</param>
/// <returns><see langword="true"/> when the service can prompt the user; otherwise <see langword="false"/>.</returns>
[AspireExport]
public static bool IsAvailable(this IInteractionService interactionService)
{
ArgumentNullException.ThrowIfNull(interactionService);
return interactionService.IsAvailable;
}
/// <summary>
/// Prompts the user for confirmation with an OK/Cancel dialog.
/// </summary>
[AspireExport]
public static async Task<BoolInteractionResult> PromptConfirmation(
this IInteractionService interactionService,
string title,
string message,
InteractionMessageBoxOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(interactionService);
var result = await interactionService.PromptConfirmationAsync(title, message, options?.ToOptions(), cancellationToken).ConfigureAwait(false);
return BoolInteractionResult.From(result);
}
/// <summary>
/// Prompts the user with a message box dialog.
/// </summary>
[AspireExport]
public static async Task<BoolInteractionResult> PromptMessageBox(
this IInteractionService interactionService,
string title,
string message,
InteractionMessageBoxOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(interactionService);
var result = await interactionService.PromptMessageBoxAsync(title, message, options?.ToOptions(), cancellationToken).ConfigureAwait(false);
return BoolInteractionResult.From(result);
}
/// <summary>
/// Prompts the user with a notification.
/// </summary>
[AspireExport]
public static async Task<BoolInteractionResult> PromptNotification(
this IInteractionService interactionService,
string title,
string message,
InteractionNotificationOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(interactionService);
var result = await interactionService.PromptNotificationAsync(title, message, options?.ToOptions(), cancellationToken).ConfigureAwait(false);
return BoolInteractionResult.From(result);
}
/// <summary>
/// Displays a progress dialog with an indeterminate progress indicator.
/// </summary>
// Progress prompts can invoke Work callbacks that re-enter the remote host through ATS, so the synchronous
// invocation path must run on a background thread to keep the JSON-RPC loop processing nested callbacks.
[AspireExport(RunSyncOnBackgroundThread = true)]
public static async Task<BoolInteractionResult> PromptProgress(
this IInteractionService interactionService,
string message,
InteractionProgressOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(interactionService);
var result = await interactionService.PromptProgressAsync(message, options?.ToOptions(), cancellationToken).ConfigureAwait(false);
return BoolInteractionResult.From(result);
}
/// <summary>
/// Prompts the user for a single input.
/// </summary>
// Prompts can invoke dynamic-loading and validation callbacks that re-enter the remote host through ATS, so the
// synchronous invocation path must run on a background thread to keep the JSON-RPC loop processing nested callbacks.
[AspireExport(RunSyncOnBackgroundThread = true)]
public static async Task<InputInteractionResult> PromptInput(
this IInteractionService interactionService,
string title,
string? message,
InteractionInputBuilder input,
InteractionInputsDialogOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(interactionService);
ArgumentNullException.ThrowIfNull(input);
var result = await interactionService.PromptInputAsync(title, message, input.Input, options?.ToOptions(), cancellationToken).ConfigureAwait(false);
return InputInteractionResult.From(result);
}
/// <summary>
/// Prompts the user for multiple inputs.
/// </summary>
// Prompts can invoke dynamic-loading and validation callbacks that re-enter the remote host through ATS, so the
// synchronous invocation path must run on a background thread to keep the JSON-RPC loop processing nested callbacks.
[AspireExport(RunSyncOnBackgroundThread = true)]
public static async Task<InputsInteractionResult> PromptInputs(
this IInteractionService interactionService,
string title,
string? message,
InteractionInputBuilder[] inputs,
InteractionInputsDialogOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(interactionService);
ArgumentNullException.ThrowIfNull(inputs);
var interactionInputs = new InteractionInput[inputs.Length];
for (var i = 0; i < inputs.Length; i++)
{
var input = inputs[i] ?? throw new ArgumentException($"The input at index {i} cannot be null.", nameof(inputs));
interactionInputs[i] = input.Input;
}
var result = await interactionService.PromptInputsAsync(title, message, interactionInputs, options?.ToOptions(), cancellationToken).ConfigureAwait(false);
return InputsInteractionResult.From(result);
}
// The input factories hang off IInteractionService so the ATS scanner treats the service handle as the
// receiver (polyglot: interactionService.createTextInput(...)). The receiver itself is unused because inputs
// are independent of the service, so suppress the unused-parameter analyzer for the factory block.
#pragma warning disable IDE0060 // Remove unused parameter
/// <summary>
/// Creates a single-line text input.
/// </summary>
[AspireExport]
public static InteractionInputBuilder CreateTextInput(this IInteractionService interactionService, string name, CreateInteractionInputOptions? options = null)
{
return InteractionInputBuilder.Create(name, InputType.Text, options);
}
/// <summary>
/// Creates a secret (masked) text input.
/// </summary>
[AspireExport]
public static InteractionInputBuilder CreateSecretInput(this IInteractionService interactionService, string name, CreateInteractionInputOptions? options = null)
{
return InteractionInputBuilder.Create(name, InputType.SecretText, options);
}
/// <summary>
/// Creates a boolean (checkbox) input.
/// </summary>
[AspireExport]
public static InteractionInputBuilder CreateBooleanInput(this IInteractionService interactionService, string name, CreateInteractionInputOptions? options = null)
{
return InteractionInputBuilder.Create(name, InputType.Boolean, options);
}
/// <summary>
/// Creates a numeric input.
/// </summary>
[AspireExport]
public static InteractionInputBuilder CreateNumberInput(this IInteractionService interactionService, string name, CreateInteractionInputOptions? options = null)
{
return InteractionInputBuilder.Create(name, InputType.Number, options);
}
/// <summary>
/// Creates a file input.
/// </summary>
[AspireExport]
public static InteractionInputBuilder CreateFileInput(this IInteractionService interactionService, string name, CreateInteractionInputOptions? options = null)
{
return InteractionInputBuilder.Create(name, InputType.File, options);
}
/// <summary>
/// Creates a choice input that selects from a list of options.
/// </summary>
/// <param name="interactionService">The interaction service.</param>
/// <param name="name">The name of the input.</param>
/// <param name="choices">The available choices, in display order. Each option pairs a submitted value with a display label.</param>
/// <param name="options">Optional configuration for the input.</param>
[AspireExport]
public static InteractionInputBuilder CreateChoiceInput(this IInteractionService interactionService, string name, IReadOnlyList<InteractionChoiceOption>? choices = null, CreateInteractionInputOptions? options = null)
{
var builder = InteractionInputBuilder.Create(name, InputType.Choice, options);
if (choices is { Count: > 0 })
{
builder.Input.Options = ToOptionList(choices);
}
return builder;
}
#pragma warning restore IDE0060 // Remove unused parameter
// Preserve the caller-specified order: the native Options list is ordered, and the order is user-visible in the
// rendered dropdown. Materialize a copy so a caller-held list cannot mutate the input after the fact.
internal static IReadOnlyList<KeyValuePair<string, string>> ToOptionList(IReadOnlyList<InteractionChoiceOption> choices)
{
var list = new List<KeyValuePair<string, string>>(choices.Count);
foreach (var choice in choices)
{
list.Add(KeyValuePair.Create(choice.Value, choice.Label));
}
return list;
}
// The engine returns the same InteractionInput instances that the builders own, and those still carry the
// dynamic-loading delegate on DynamicLoading.LoadCallback. That delegate is a .NET Func that cannot be
// serialized across the ATS/JSON-RPC boundary, so project result inputs onto callback-free copies before they
// are sent back to the polyglot caller. The caller only consumes data fields such as Name, Value and Options.
internal static InteractionInput ToResultInput(InteractionInput input)
{
var result = new InteractionInput
{
Name = input.Name,
Label = input.Label,
Description = input.Description,
EnableDescriptionMarkdown = input.EnableDescriptionMarkdown,
InputType = input.InputType,
Required = input.Required,
Options = input.Options,
Value = input.Value,
Placeholder = input.Placeholder,
AllowCustomChoice = input.AllowCustomChoice,
Disabled = input.Disabled,
MaxLength = input.MaxLength,
MaxFileSize = input.MaxFileSize,
AllowMultipleFiles = input.AllowMultipleFiles,
FileFilter = input.FileFilter,
// DynamicLoading is intentionally omitted: it holds the non-serializable LoadCallback delegate.
};
var files = input.GetFiles();
if (files.Count > 0)
{
result.SetFiles(new InteractionFileCollection(files));
}
return result;
}
}
/// <summary>
/// An opaque, server-side builder for an <see cref="InteractionInput"/> used by polyglot app hosts.
/// </summary>
/// <remarks>
/// The builder owns the live <see cref="InteractionInput"/> instance. Dynamic-loading callbacks mutate this same
/// instance through <see cref="InteractionInputLoadContext"/>, which is why the input is modeled as a handle here
/// instead of the by-value <c>InteractionInput</c> DTO.
/// </remarks>
[AspireExport]
internal sealed class InteractionInputBuilder
{
private InteractionInputBuilder(InteractionInput input)
{
Input = input;
}
internal InteractionInput Input { get; }
internal static InteractionInputBuilder Create(string name, InputType inputType, CreateInteractionInputOptions? options)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
var input = new InteractionInput
{
Name = name,
InputType = inputType,
Label = options?.Label,
Description = options?.Description,
EnableDescriptionMarkdown = options?.EnableDescriptionMarkdown ?? false,
Required = options?.Required ?? false,
Placeholder = options?.Placeholder,
Value = options?.Value,
AllowCustomChoice = options?.AllowCustomChoice ?? false,
Disabled = options?.Disabled ?? false,
MaxLength = options?.MaxLength,
MaxFileSize = options?.MaxFileSize,
AllowMultipleFiles = options?.AllowMultipleFiles ?? false,
FileFilter = options?.FileFilter,
};
return new InteractionInputBuilder(input);
}
/// <summary>
/// Sets the choice options for the input.
/// </summary>
/// <param name="choices">The available choices, in display order. Each option pairs a submitted value with a display label.</param>
/// <returns>The same builder handle.</returns>
[AspireExport]
public InteractionInputBuilder WithChoiceOptions(IReadOnlyList<InteractionChoiceOption> choices)
{
ArgumentNullException.ThrowIfNull(choices);
Input.Options = InteractionExports.ToOptionList(choices);
return this;
}
/// <summary>
/// Sets the value of the input.
/// </summary>
/// <param name="value">The value to assign.</param>
/// <returns>The same builder handle.</returns>
[AspireExport]
public InteractionInputBuilder WithValue(string? value)
{
Input.Value = value;
return this;
}
/// <summary>
/// Releases uploaded files associated with the input.
/// </summary>
/// <remarks>
/// Call this after processing the file paths returned by the prompt. Releasing the files deletes the
/// server-side temporary files before AppHost shutdown and is idempotent. Files that are not released are
/// deleted when the AppHost shuts down.
/// </remarks>
[AspireExport]
public void ReleaseFiles()
{
Input.GetFiles().Dispose();
}
/// <summary>
/// Attaches a callback that dynamically loads or updates the input after the prompt starts.
/// </summary>
/// <param name="callback">The callback invoked to load the input. Use the supplied context to read other inputs and update this input.</param>
/// <param name="options">Optional configuration that controls when the callback runs.</param>
/// <returns>The same builder handle.</returns>
[AspireExport]
public InteractionInputBuilder WithDynamicLoading(Func<InteractionInputLoadContext, Task> callback, DynamicLoadingOptions? options = null)
{
ArgumentNullException.ThrowIfNull(callback);
// Bridge the engine's LoadInputContext to the curated polyglot context so callbacks never see the raw
// IServiceProvider and can only mutate the live input through guarded setters.
Input.SetDynamicLoading(new InputLoadOptions
{
LoadCallback = loadContext => callback(new InteractionInputLoadContext(loadContext)),
AlwaysLoadOnStart = options?.AlwaysLoadOnStart ?? false,
DependsOnInputs = options?.DependsOnInputs,
});
return this;
}
}
/// <summary>
/// The context passed to a polyglot dynamic-loading callback. Exposes the loading input as a handle and provides
/// read access to the other inputs in the prompt.
/// </summary>
[AspireExport(ExposeProperties = true)]
internal sealed class InteractionInputLoadContext
{
private readonly LoadInputContext _inner;
private readonly InteractionLoadingInput _input;
internal InteractionInputLoadContext(LoadInputContext inner)
{
_inner = inner;
_input = new InteractionLoadingInput(inner);
}
/// <summary>
/// Gets a handle to the input that is loading. Mutate the input through this handle.
/// </summary>
/// <returns>A handle to the loading input.</returns>
/// <remarks>
/// Mirrors the native <c>LoadInputContext.Input</c>: the callback updates the live input it is loading, rather than
/// the context itself. The input is a handle (not a by-value DTO) so guarded setters route back to the server-side
/// input across the ATS boundary.
/// </remarks>
[AspireExport]
public InteractionLoadingInput Input()
{
return _input;
}
/// <summary>
/// Gets all inputs in the prompt, including the one currently loading.
/// </summary>
/// <remarks>
/// Mirrors the native <c>LoadInputContext.AllInputs</c>. Use the collection's by-name accessors (for example
/// <c>value</c> or <c>requiredValue</c>) to read the dependency inputs declared via
/// <see cref="DynamicLoadingOptions.DependsOnInputs"/>. This is the same <see cref="InteractionInputCollection"/>
/// idiom used by the validation callback and prompt results, so reading inputs by name is consistent across every
/// callback context. This is exposed as a property (rather than a method) so it routes through the generated
/// collection accessor, matching the other contexts that surface an <see cref="InteractionInputCollection"/>.
/// </remarks>
public InteractionInputCollection Inputs => _inner.AllInputs;
}
/// <summary>
/// A handle to the input currently being loaded by a dynamic-loading callback. Mirrors the native
/// <c>LoadInputContext.Input</c> by letting callbacks update the live input directly.
/// </summary>
/// <remarks>
/// The handle owns the live <see cref="InteractionInput"/> for the duration of the load callback. Setters are routed
/// back to the server-side input across the ATS boundary, which is why this is a handle rather than the by-value
/// <c>InteractionInput</c> DTO.
/// </remarks>
[AspireExport]
internal sealed class InteractionLoadingInput
{
private readonly LoadInputContext _inner;
internal InteractionLoadingInput(LoadInputContext inner)
{
_inner = inner;
}
/// <summary>
/// Gets the name of the input.
/// </summary>
/// <returns>The input name.</returns>
[AspireExport]
public string GetName()
{
return _inner.Input.Name;
}
/// <summary>
/// Sets the choice options for the input.
/// </summary>
/// <param name="choices">The available choices, in display order. Each option pairs a submitted value with a display label.</param>
[AspireExport]
public void SetChoiceOptions(IReadOnlyList<InteractionChoiceOption> choices)
{
ArgumentNullException.ThrowIfNull(choices);
// Honor cancellation so a stale load that was superseded by a newer one does not overwrite the input.
_inner.CancellationToken.ThrowIfCancellationRequested();
_inner.Input.Options = InteractionExports.ToOptionList(choices);
}
/// <summary>
/// Sets the value of the input.
/// </summary>
/// <param name="value">The value to assign.</param>
[AspireExport]
public void SetValue(string? value)
{
_inner.CancellationToken.ThrowIfCancellationRequested();
_inner.Input.Value = value;
}
}
/// <summary>
/// A single selectable option for a choice input. Options are presented in the order supplied.
/// </summary>
[AspireDto]
internal sealed class InteractionChoiceOption
{
/// <summary>
/// Gets or sets the value submitted when this option is selected.
/// </summary>
public string Value { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the label displayed for this option.
/// </summary>
public string Label { get; set; } = string.Empty;
}
/// <summary>
/// Optional configuration shared by interaction input factory capabilities.
/// </summary>
[AspireDto]
internal sealed class CreateInteractionInputOptions
{
/// <summary>
/// Gets or sets the label for the input. Defaults to the input name when not specified.
/// </summary>
public string? Label { get; init; }
/// <summary>
/// Gets or sets the description for the input.
/// </summary>
public string? Description { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the description is rendered as Markdown.
/// </summary>
public bool? EnableDescriptionMarkdown { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the input is required.
/// </summary>
public bool? Required { get; init; }
/// <summary>
/// Gets or sets the placeholder text for the input.
/// </summary>
public string? Placeholder { get; init; }
/// <summary>
/// Gets or sets the initial value of the input.
/// </summary>
public string? Value { get; init; }
/// <summary>
/// Gets or sets a value indicating whether a custom choice is allowed. Only used by choice inputs.
/// </summary>
public bool? AllowCustomChoice { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the input is disabled.
/// </summary>
public bool? Disabled { get; init; }
/// <summary>
/// Gets or sets the maximum length for text inputs.
/// </summary>
public int? MaxLength { get; init; }
/// <summary>
/// Gets or sets the maximum file size in bytes for file inputs.
/// </summary>
public long? MaxFileSize { get; init; }
/// <summary>
/// Gets or sets a value indicating whether multiple files can be selected. Only used by file inputs.
/// </summary>
public bool? AllowMultipleFiles { get; init; }
/// <summary>
/// Gets or sets the file type filter for file inputs. Uses the same format as the HTML accept attribute.
/// The CLI validates only dot-prefixed extension filters and does not validate MIME type patterns such as "image/*".
/// </summary>
public string? FileFilter { get; init; }
}
/// <summary>
/// Options controlling when a dynamic-loading callback runs.
/// </summary>
[AspireDto]
internal sealed class DynamicLoadingOptions
{
/// <summary>
/// Gets or sets a value indicating whether the callback always runs at the start of the prompt.
/// </summary>
public bool? AlwaysLoadOnStart { get; init; }
/// <summary>
/// Gets or sets the names of inputs this input depends on. The callback runs when any of them change.
/// </summary>
public IReadOnlyList<string>? DependsOnInputs { get; init; }
}
/// <summary>
/// Options for message box and confirmation prompts.
/// </summary>
[AspireDto]
internal sealed class InteractionMessageBoxOptions
{
/// <summary>
/// Gets or sets the primary button text.
/// </summary>
public string? PrimaryButtonText { get; init; }
/// <summary>
/// Gets or sets the secondary button text.
/// </summary>
public string? SecondaryButtonText { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the secondary button is shown.
/// </summary>
public bool? ShowSecondaryButton { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the dismiss button is shown.
/// </summary>
public bool? ShowDismiss { get; init; }
/// <summary>
/// Gets or sets a value indicating whether Markdown in the message is rendered.
/// </summary>
public bool? EnableMessageMarkdown { get; init; }
/// <summary>
/// Gets or sets the intent of the message box.
/// </summary>
public MessageIntent? Intent { get; init; }
internal MessageBoxInteractionOptions ToOptions()
{
return new MessageBoxInteractionOptions
{
PrimaryButtonText = PrimaryButtonText,
SecondaryButtonText = SecondaryButtonText,
ShowSecondaryButton = ShowSecondaryButton,
ShowDismiss = ShowDismiss,
EnableMessageMarkdown = EnableMessageMarkdown,
Intent = Intent,
};
}
}
/// <summary>
/// Options for notification prompts.
/// </summary>
[AspireDto]
internal sealed class InteractionNotificationOptions
{
/// <summary>
/// Gets or sets the primary button text.
/// </summary>
public string? PrimaryButtonText { get; init; }
/// <summary>
/// Gets or sets the secondary button text.
/// </summary>
public string? SecondaryButtonText { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the secondary button is shown.
/// </summary>
public bool? ShowSecondaryButton { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the dismiss button is shown.
/// </summary>
public bool? ShowDismiss { get; init; }
/// <summary>
/// Gets or sets a value indicating whether Markdown in the message is rendered.
/// </summary>
public bool? EnableMessageMarkdown { get; init; }
/// <summary>
/// Gets or sets the intent of the notification.
/// </summary>
public MessageIntent? Intent { get; init; }
/// <summary>
/// Gets or sets the text for a link in the notification.
/// </summary>
public string? LinkText { get; init; }
/// <summary>
/// Gets or sets the URL for the link in the notification.
/// </summary>
public string? LinkUrl { get; init; }
internal NotificationInteractionOptions ToOptions()
{
return new NotificationInteractionOptions
{
PrimaryButtonText = PrimaryButtonText,
SecondaryButtonText = SecondaryButtonText,
ShowSecondaryButton = ShowSecondaryButton,
ShowDismiss = ShowDismiss,
EnableMessageMarkdown = EnableMessageMarkdown,
Intent = Intent,
LinkText = LinkText,
LinkUrl = LinkUrl,
};
}
}
/// <summary>
/// Options for inputs dialog prompts.
/// </summary>
[AspireDto]
internal sealed class InteractionInputsDialogOptions
{
/// <summary>
/// Gets or sets the primary button text.
/// </summary>
public string? PrimaryButtonText { get; init; }
/// <summary>
/// Gets or sets the secondary button text.
/// </summary>
public string? SecondaryButtonText { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the secondary button is shown.
/// </summary>
public bool? ShowSecondaryButton { get; init; }
/// <summary>
/// Gets or sets a value indicating whether the dismiss button is shown.
/// </summary>
public bool? ShowDismiss { get; init; }
/// <summary>
/// Gets or sets a value indicating whether Markdown in the message is rendered.
/// </summary>
public bool? EnableMessageMarkdown { get; init; }
/// <summary>
/// Gets or sets a callback invoked to validate the inputs before the dialog is accepted. The callback
/// receives a validation context that exposes the current inputs and can record validation errors.
/// </summary>
public Func<InputsDialogValidationContext, Task>? ValidationCallback { get; init; }
internal InputsDialogInteractionOptions ToOptions()
{
return new InputsDialogInteractionOptions
{
PrimaryButtonText = PrimaryButtonText,
SecondaryButtonText = SecondaryButtonText,
ShowSecondaryButton = ShowSecondaryButton,
ShowDismiss = ShowDismiss,
EnableMessageMarkdown = EnableMessageMarkdown,
ValidationCallback = ValidationCallback,
};
}
}
/// <summary>
/// Options for progress dialog prompts.
/// </summary>
[AspireDto]
internal sealed class InteractionProgressOptions
{
/// <summary>
/// Gets or sets the optional title of the progress dialog.
/// </summary>
public string? Title { get; init; }
/// <summary>
/// Gets or sets the primary button text (e.g. "Cancel").
/// </summary>
public string? PrimaryButtonText { get; init; }
/// <summary>
/// Gets or sets a value indicating whether Markdown in the message is rendered.
/// </summary>
public bool? EnableMessageMarkdown { get; init; }
/// <summary>
/// Gets or sets an optional asynchronous work callback to execute while the progress dialog is displayed.
/// When provided, the progress dialog remains open while this callback executes and closes automatically
/// when the callback completes.
/// </summary>
public Func<ProgressContext, Task>? Work { get; init; }
internal ProgressInteractionOptions ToOptions()
{
return new ProgressInteractionOptions
{
Title = Title,
PrimaryButtonText = PrimaryButtonText,
EnableMessageMarkdown = EnableMessageMarkdown,
Work = Work,
};
}
}
/// <summary>
/// The result of a boolean interaction prompt.
/// </summary>
[AspireDto]
internal sealed class BoolInteractionResult
{
/// <summary>
/// Gets a value indicating whether the interaction was canceled by the user.
/// </summary>
public required bool Canceled { get; init; }
/// <summary>
/// Gets the value returned from the interaction. Not meaningful when <see cref="Canceled"/> is <see langword="true"/>.
/// </summary>
public bool Value { get; init; }
internal static BoolInteractionResult From(InteractionResult<bool> result)
{
return new BoolInteractionResult
{
Canceled = result.Canceled,
Value = !result.Canceled && result.Data,
};
}
}
/// <summary>
/// The result of a single-input interaction prompt.
/// </summary>
[AspireDto]
internal sealed class InputInteractionResult
{
/// <summary>
/// Gets a value indicating whether the interaction was canceled by the user.
/// </summary>
public required bool Canceled { get; init; }
/// <summary>
/// Gets the input returned from the interaction. Not present when <see cref="Canceled"/> is <see langword="true"/>.
/// </summary>
public InteractionInput? Input { get; init; }
internal static InputInteractionResult From(InteractionResult<InteractionInput> result)
{
return new InputInteractionResult
{
Canceled = result.Canceled,
Input = result.Canceled || result.Data is null ? null : InteractionExports.ToResultInput(result.Data),
};
}
}
/// <summary>
/// The result of a multi-input interaction prompt.
/// </summary>
/// <remarks>
/// Modeled as a handle (not a by-value DTO) so the returned inputs are surfaced as the
/// <see cref="InteractionInputCollection"/> handle. That lets polyglot callers reuse the same name-based
/// accessors (for example <c>result.inputs().value("color")</c>) that the validation and command-argument
/// collections already expose, instead of having to scan a serialized array by hand.
/// </remarks>
[AspireExport(ExposeProperties = true)]
internal sealed class InputsInteractionResult
{
/// <summary>
/// Gets a value indicating whether the interaction was canceled by the user.
/// </summary>
public required bool Canceled { get; init; }
/// <summary>
/// Gets the inputs returned from the interaction. Empty when <see cref="Canceled"/> is <see langword="true"/>.
/// </summary>
public required InteractionInputCollection Inputs { get; init; }
internal static InputsInteractionResult From(InteractionResult<InteractionInputCollection> result)
{
// The engine returns the live input instances, which still carry the non-serializable dynamic-loading
// callback on DynamicLoading. Project onto callback-free copies (ToResultInput) before wrapping them in a
// fresh collection so the handle can be enumerated/serialized safely after the prompt completes.
var inputs = result.Canceled || result.Data is null
? new InteractionInputCollection([])
: new InteractionInputCollection(result.Data.Select(InteractionExports.ToResultInput).ToArray());
return new InputsInteractionResult
{
Canceled = result.Canceled,
Inputs = inputs,
};
}
}