File: Backchannel\BackchannelDataTypes.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
// These types are source shared between the CLI and the Aspire.Hosting projects.
// The CLI sets the types in its own namespace.
#if CLI
namespace Aspire.Cli.Backchannel;
#else
namespace Aspire.Hosting.Backchannel;
#endif
 
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
 
// =============================================================================
// Auxiliary Backchannel Contract Rules:
//
// 1. All methods take a single request object (nullable where sensible)
// 2. All methods return a response object (or IAsyncEnumerable<T> for streaming)
// 3. Request types derive from BackchannelRequest; request/response types are sealed classes with { get; init; } properties
// 4. Required properties use 'required' keyword
// 5. Optional properties are nullable (T?) - can be added without breaking
// 6. Empty request classes are allowed (for future expansion)
// 7. Method names: Get*Async, Watch*Async (streaming), Call*Async (actions)
// =============================================================================
 
#region Capability Constants
 
/// <summary>
/// Constants for auxiliary backchannel capability versions.
/// </summary>
internal static class AuxiliaryBackchannelCapabilities
{
    /// <summary>
    /// Version 1 capabilities (13.1 baseline): GetAppHostInformationAsync, GetDashboardMcpConnectionInfoAsync, StopAppHostAsync.
    /// </summary>
    public const string V1 = "aux.v1";
 
    /// <summary>
    /// Version 2 capabilities (13.2+): Request objects, new methods.
    /// </summary>
    public const string V2 = "aux.v2";
 
    /// <summary>
    /// Version 3 capabilities: Batched console log streaming, AppHost startup readiness wait,
    /// and JSON-valued resource properties when requested by the client.
    /// </summary>
    public const string V3 = "aux.v3";
 
    /// <summary>
    /// Terminal capability (13.4+): the AppHost exposes per-replica terminal info via
    /// <see cref="GetTerminalInfoResponse.Replicas"/> AND the per-resource list returned by
    /// <c>ListTerminalsAsync</c> (with per-replica current grid size and attached-peer counts).
    /// Together these surfaces back <c>aspire terminal attach</c> and <c>aspire terminal ps</c>.
    /// The two were split during development as <c>terminals.v1</c> and <c>terminals.ps.v1</c>;
    /// they were consolidated before ship because they are interconnected and always ship together.
    /// Older clients ignore the new fields/RPC; new clients gate CLI/UI affordances on this capability.
    /// </summary>
    public const string Terminals_V1 = "terminals.v1";
 
    /// <summary>
    /// Resource snapshot version capability: snapshots include monotonic versions that can be used for ordering.
    /// </summary>
    public const string ResourceSnapshotVersions_V1 = "resource-snapshot-versions.v1";
}
 
/// <summary>
/// Constants for resource command visibility values in the auxiliary backchannel contract.
/// </summary>
internal static class KnownCommandVisibility
{
    public const string UI = "UI";
    public const string Api = "Api";
    public const string Default = $"{UI}, {Api}";
}
 
#endregion
 
#region V2 Request/Response Types
 
/// <summary>
/// Trace context metadata propagated over the auxiliary backchannel.
/// </summary>
internal sealed class BackchannelTraceContext
{
    /// <summary>
    /// Gets the W3C traceparent value associated with the caller span.
    /// </summary>
    public string? TraceParent { get; init; }
 
    /// <summary>
    /// Gets the W3C tracestate value associated with the caller span.
    /// </summary>
    public string? TraceState { get; init; }
 
    /// <summary>
    /// Gets the baggage values associated with the trace.
    /// </summary>
    public Dictionary<string, string> Baggage { get; init; } = [];
}
 
/// <summary>
/// Base class for auxiliary backchannel request-object RPC parameters.
/// </summary>
internal abstract class BackchannelRequest
{
    /// <summary>
    /// Gets trace context metadata propagated by the CLI.
    /// </summary>
    public BackchannelTraceContext? TraceContext { get; init; }
 
    /// <summary>
    /// Creates a copy of this request with the specified trace context.
    /// </summary>
    /// <remarks>
    /// StreamJsonRpc carries W3C traceparent/tracestate on the JSON-RPC request envelope.
    /// See https://microsoft.github.io/vs-streamjsonrpc/docs/resiliency.html#activity-tracing.
    /// The request object only carries extra trace metadata such as baggage values. Each
    /// request type owns its copy logic so this stays AOT- and trimming-friendly instead of
    /// relying on reflection to clone arbitrary records/classes.
    /// </remarks>
    public abstract BackchannelRequest WithTraceContext(BackchannelTraceContext traceContext);
}
 
/// <summary>
/// Request for getting auxiliary backchannel capabilities.
/// </summary>
internal sealed class GetCapabilitiesRequest : BackchannelRequest
{
    /// <inheritdoc />
    public override GetCapabilitiesRequest WithTraceContext(BackchannelTraceContext traceContext) => new() { TraceContext = traceContext };
}
 
/// <summary>
/// Response containing auxiliary backchannel capabilities.
/// </summary>
internal sealed class GetCapabilitiesResponse
{
    /// <summary>
    /// Gets the list of supported capability versions (e.g., "aux.v1", "aux.v2").
    /// </summary>
    public required string[] Capabilities { get; init; }
}
 
/// <summary>
/// Request for getting AppHost information.
/// </summary>
internal sealed class GetAppHostInfoRequest : BackchannelRequest
{
    /// <inheritdoc />
    public override GetAppHostInfoRequest WithTraceContext(BackchannelTraceContext traceContext) => new() { TraceContext = traceContext };
}
 
/// <summary>
/// Response containing AppHost information.
/// </summary>
internal sealed class GetAppHostInfoResponse
{
    /// <summary>
    /// Gets the AppHost process ID.
    /// </summary>
    public required string Pid { get; init; }
 
    /// <summary>
    /// Gets the Aspire hosting version.
    /// </summary>
    public required string AspireHostVersion { get; init; }
 
    /// <summary>
    /// Gets the fully qualified path to the AppHost project.
    /// </summary>
    public required string AppHostPath { get; init; }
 
    /// <summary>
    /// Gets the CLI process ID if the AppHost was launched via the CLI.
    /// </summary>
    public int? CliProcessId { get; init; }
 
    /// <summary>
    /// Gets when the AppHost process started.
    /// </summary>
    public DateTimeOffset? StartedAt { get; init; }
 
    /// <summary>
    /// Gets the log file path of the CLI process that launched the AppHost, if applicable.
    /// </summary>
    public string? CliLogFilePath { get; init; }
}
 
/// <summary>
/// Request for getting Dashboard information.
/// </summary>
internal sealed class GetDashboardInfoRequest : BackchannelRequest
{
    /// <inheritdoc />
    public override GetDashboardInfoRequest WithTraceContext(BackchannelTraceContext traceContext) => new() { TraceContext = traceContext };
}
 
/// <summary>
/// Response containing Dashboard information.
/// </summary>
internal sealed class GetDashboardInfoResponse
{
    /// <summary>
    /// Gets the base URL of the Dashboard API (without login token).
    /// Use this for API calls like /api/telemetry/*.
    /// </summary>
    public string? ApiBaseUrl { get; init; }
 
    /// <summary>
    /// Gets the Dashboard API token for authenticated API calls.
    /// </summary>
    public string? ApiToken { get; init; }
 
    /// <summary>
    /// Gets the Dashboard URLs with login tokens.
    /// </summary>
    public required string[] DashboardUrls { get; init; }
 
    /// <summary>
    /// Gets whether the Dashboard is healthy.
    /// </summary>
    public bool IsHealthy { get; init; }
}
 
/// <summary>
/// Request for waiting until the AppHost reaches its startup readiness point.
/// </summary>
internal sealed class WaitForAppHostReadyRequest : BackchannelRequest
{
    /// <inheritdoc />
    public override WaitForAppHostReadyRequest WithTraceContext(BackchannelTraceContext traceContext) => new() { TraceContext = traceContext };
}
 
/// <summary>
/// Response returned once the AppHost reaches its startup readiness point.
/// </summary>
internal sealed class WaitForAppHostReadyResponse
{
    /// <summary>
    /// Gets whether the AppHost has reached its startup readiness point.
    /// </summary>
    public bool IsReady { get; init; }
}
 
/// <summary>
/// Request for getting resource snapshots.
/// </summary>
internal sealed class GetResourcesRequest : BackchannelRequest
{
    /// <summary>
    /// Gets an optional filter pattern for resource names.
    /// </summary>
    public string? Filter { get; init; }
 
    /// <summary>
    /// Gets the auxiliary backchannel capabilities supported by the client.
    /// </summary>
    public string[] ClientCapabilities { get; init; } = [];
 
    /// <inheritdoc />
    public override GetResourcesRequest WithTraceContext(BackchannelTraceContext traceContext) => new()
    {
        TraceContext = traceContext,
        Filter = Filter,
        ClientCapabilities = ClientCapabilities
    };
}
 
/// <summary>
/// Response containing resource snapshots.
/// </summary>
internal sealed class GetResourcesResponse
{
    /// <summary>
    /// Gets the resource snapshots.
    /// </summary>
    public required ResourceSnapshot[] Resources { get; init; }
}
 
/// <summary>
/// Request for watching resource changes.
/// </summary>
internal sealed class WatchResourcesRequest : BackchannelRequest
{
    /// <summary>
    /// Gets an optional filter pattern for resource names.
    /// </summary>
    public string? Filter { get; init; }
 
    /// <summary>
    /// Gets the auxiliary backchannel capabilities supported by the client.
    /// </summary>
    public string[] ClientCapabilities { get; init; } = [];
 
    /// <inheritdoc />
    public override WatchResourcesRequest WithTraceContext(BackchannelTraceContext traceContext) => new()
    {
        TraceContext = traceContext,
        Filter = Filter,
        ClientCapabilities = ClientCapabilities
    };
}
 
/// <summary>
/// Request for getting console logs.
/// </summary>
internal sealed class GetConsoleLogsRequest : BackchannelRequest
{
    /// <summary>
    /// Gets the resource name to get logs for.
    /// </summary>
    public string? ResourceName { get; init; }
 
    /// <summary>
    /// Gets whether to follow (stream) new log entries.
    /// </summary>
    public bool Follow { get; init; }
 
    /// <summary>
    /// Gets an optional search string to match against log content or resource name.
    /// </summary>
    public string? Search { get; init; }
 
    /// <summary>
    /// Gets the maximum number of matching snapshot log entries to return.
    /// </summary>
    public int? Tail { get; init; }
 
    /// <summary>
    /// Gets whether hidden resources should be included when no resource name is specified.
    /// </summary>
    public bool IncludeHidden { get; init; }
 
    /// <inheritdoc />
    public override GetConsoleLogsRequest WithTraceContext(BackchannelTraceContext traceContext) => new()
    {
        TraceContext = traceContext,
        ResourceName = ResourceName,
        Follow = Follow,
        Search = Search,
        Tail = Tail,
        IncludeHidden = IncludeHidden
    };
}
 
/// <summary>
/// Request for calling an MCP tool on a resource.
/// </summary>
internal sealed class CallMcpToolRequest : BackchannelRequest
{
    /// <summary>
    /// Gets the resource name.
    /// </summary>
    public required string ResourceName { get; init; }
 
    /// <summary>
    /// Gets the tool name.
    /// </summary>
    public required string ToolName { get; init; }
 
    /// <summary>
    /// Gets the tool arguments.
    /// </summary>
    public JsonElement? Arguments { get; init; }
 
    /// <inheritdoc />
    public override CallMcpToolRequest WithTraceContext(BackchannelTraceContext traceContext) => new()
    {
        TraceContext = traceContext,
        ResourceName = ResourceName,
        ToolName = ToolName,
        Arguments = Arguments
    };
}
 
/// <summary>
/// Response from calling an MCP tool.
/// </summary>
internal sealed class CallMcpToolResponse
{
    /// <summary>
    /// Gets whether the tool call resulted in an error.
    /// </summary>
    public required bool IsError { get; init; }
 
    /// <summary>
    /// Gets the content items returned by the tool.
    /// </summary>
    public required McpToolContentItem[] Content { get; init; }
}
 
/// <summary>
/// Represents a content item returned by an MCP tool.
/// </summary>
internal sealed class McpToolContentItem
{
    /// <summary>
    /// Gets the content type (e.g., "text").
    /// </summary>
    public required string Type { get; init; }
 
    /// <summary>
    /// Gets the text content.
    /// </summary>
    public string? Text { get; init; }
}
 
/// <summary>
/// Request for stopping the AppHost.
/// </summary>
internal sealed class StopAppHostRequest : BackchannelRequest
{
    /// <summary>
    /// Gets the exit code to use when stopping.
    /// </summary>
    public int? ExitCode { get; init; }
 
    /// <inheritdoc />
    public override StopAppHostRequest WithTraceContext(BackchannelTraceContext traceContext) => new()
    {
        TraceContext = traceContext,
        ExitCode = ExitCode
    };
}
 
/// <summary>
/// Response from stopping the AppHost.
/// </summary>
internal sealed class StopAppHostResponse { }
 
/// <summary>
/// Request for executing a resource command.
/// </summary>
internal sealed class ExecuteResourceCommandRequest : BackchannelRequest
{
    /// <summary>
    /// Gets the resource name (or resource ID for replicas).
    /// </summary>
    public required string ResourceName { get; init; }
 
    /// <summary>
    /// Gets the command name (e.g., "start", "stop", "restart").
    /// </summary>
    public required string CommandName { get; init; }
 
    /// <summary>
    /// Gets optional invocation arguments to pass to the resource command.
    /// Arrays are matched to declared command arguments by order. Objects are matched by argument name.
    /// </summary>
    public JsonNode? Arguments { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether the request should validate arguments without executing the command.
    /// </summary>
    public bool ValidateOnly { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether command execution should fail instead of prompting for missing input.
    /// </summary>
    public bool NonInteractive { get; init; } = true;
 
    /// <summary>
    /// Gets a value indicating whether the response should include command argument input metadata after dynamic loading.
    /// </summary>
    public bool ReturnArgumentInputs { get; init; }
 
    /// <inheritdoc />
    public override ExecuteResourceCommandRequest WithTraceContext(BackchannelTraceContext traceContext) => new()
    {
        TraceContext = traceContext,
        ResourceName = ResourceName,
        CommandName = CommandName,
        Arguments = Arguments,
        ValidateOnly = ValidateOnly,
        NonInteractive = NonInteractive,
        ReturnArgumentInputs = ReturnArgumentInputs
    };
}
 
/// <summary>
/// Options for executing a resource command through the auxiliary backchannel.
/// </summary>
internal sealed class ExecuteResourceCommandOptions
{
    /// <summary>
    /// Gets optional invocation arguments to pass to the resource command.
    /// Arrays are matched to declared command arguments by order. Objects are matched by argument name.
    /// </summary>
    public JsonNode? Arguments { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether the request should validate arguments without executing the command.
    /// </summary>
    public bool ValidateOnly { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether command execution should fail instead of prompting for missing input.
    /// </summary>
    public bool NonInteractive { get; init; } = true;
 
    /// <summary>
    /// Gets a value indicating whether the response should include command argument input metadata after dynamic loading.
    /// </summary>
    public bool ReturnArgumentInputs { get; init; }
}
 
/// <summary>
/// Response from executing a resource command.
/// </summary>
internal sealed class ExecuteResourceCommandResponse
{
    /// <summary>
    /// Gets whether the command executed successfully.
    /// </summary>
    public required bool Success { get; init; }
 
    /// <summary>
    /// Gets whether the command was canceled.
    /// </summary>
    public bool Canceled { get; init; }
 
    /// <summary>
    /// Gets the error message if the command failed.
    /// </summary>
    [Obsolete("Use Message instead.")]
    public string? ErrorMessage { get; init; }
 
    /// <summary>
    /// Gets the message associated with the command result.
    /// </summary>
    public string? Message { get; init; }
 
    /// <summary>
    /// Gets the value produced by the command.
    /// </summary>
    public ExecuteResourceCommandResult? Value { get; init; }
 
    /// <summary>
    /// Gets validation errors for submitted command arguments.
    /// </summary>
    public ResourceCommandArgumentValidationError[] ValidationErrors { get; init; } = [];
 
    /// <summary>
    /// Gets command argument input metadata after dynamic loading has run.
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public ResourceSnapshotCommandArgument[]? ArgumentInputs { get; init; }
}
 
/// <summary>
/// Represents a validation error for a submitted resource command argument.
/// </summary>
internal sealed class ResourceCommandArgumentValidationError
{
    /// <summary>
    /// Gets the argument name.
    /// </summary>
    public required string ArgumentName { get; init; }
 
    /// <summary>
    /// Gets the validation error message.
    /// </summary>
    public required string ErrorMessage { get; init; }
}
 
/// <summary>
/// Value produced by a resource command.
/// </summary>
internal sealed class ExecuteResourceCommandResult
{
    /// <summary>
    /// Gets the value data.
    /// </summary>
    public required string Value { get; init; }
 
    /// <summary>
    /// Gets the format of the value data.
    /// </summary>
    public CommandResultFormat Format { get; init; }
 
    /// <summary>
    /// Gets whether to immediately display the value in the dashboard.
    /// </summary>
    public bool DisplayImmediately { get; init; }
}
 
/// <summary>
/// Specifies the format of a command result.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<CommandResultFormat>))]
internal enum CommandResultFormat
{
    /// <summary>
    /// Plain text result.
    /// </summary>
    [JsonStringEnumMemberName("text")]
    Text,
 
    /// <summary>
    /// JSON result.
    /// </summary>
    [JsonStringEnumMemberName("json")]
    Json,
 
    /// <summary>
    /// Markdown result.
    /// </summary>
    [JsonStringEnumMemberName("markdown")]
    Markdown
}
 
#endregion
 
#region Wait For Resource
 
/// <summary>
/// Request to wait for a resource to reach a target status.
/// </summary>
internal sealed class WaitForResourceRequest : BackchannelRequest
{
    /// <summary>
    /// Gets the name of the resource to wait for.
    /// </summary>
    public required string ResourceName { get; init; }
 
    /// <summary>
    /// Gets the target status to wait for (e.g., "up", "healthy", "down").
    /// </summary>
    public required string Status { get; init; }
 
    /// <summary>
    /// Gets the timeout in seconds.
    /// </summary>
    public int TimeoutSeconds { get; init; } = 120;
 
    /// <inheritdoc />
    public override WaitForResourceRequest WithTraceContext(BackchannelTraceContext traceContext) => new()
    {
        TraceContext = traceContext,
        ResourceName = ResourceName,
        Status = Status,
        TimeoutSeconds = TimeoutSeconds
    };
}
 
/// <summary>
/// Response from waiting for a resource.
/// </summary>
internal sealed class WaitForResourceResponse
{
    /// <summary>
    /// Gets whether the resource reached the target status.
    /// </summary>
    public required bool Success { get; init; }
 
    /// <summary>
    /// Gets the current state of the resource.
    /// </summary>
    public string? State { get; init; }
 
    /// <summary>
    /// Gets the current health status of the resource.
    /// </summary>
    public string? HealthStatus { get; init; }
 
    /// <summary>
    /// Gets whether the resource was not found.
    /// </summary>
    public bool ResourceNotFound { get; init; }
 
    /// <summary>
    /// Gets whether the wait timed out.
    /// </summary>
    public bool TimedOut { get; init; }
 
    /// <summary>
    /// Gets the error message if the wait failed.
    /// </summary>
    public string? ErrorMessage { get; init; }
}
 
#endregion
 
/// <summary>
/// Represents the state of a resource reported via RPC.
/// </summary>
internal sealed class RpcResourceState
{
    /// <summary>
    /// Gets the name of the resource.
    /// </summary>
    public required string Resource { get; init; }
 
    /// <summary>
    /// Gets the type of the resource.
    /// </summary>
    public required string Type { get; init; }
 
    /// <summary>
    /// Gets the state of the resource.
    /// </summary>
    public required string State { get; init; }
 
    /// <summary>
    /// Gets the endpoints associated with the resource.
    /// </summary>
    public required string[] Endpoints { get; init; }
 
    /// <summary>
    /// Gets the health status of the resource.
    /// </summary>
    public string? Health { get; init; }
}
 
/// <summary>
/// Represents dashboard URLs for the running AppHost.
/// </summary>
internal sealed class DashboardUrlsState
{
    public bool DashboardHealthy { get; init; } = true;
 
    /// <summary>
    /// Gets the dashboard URL.
    /// When browser token authentication is enabled, this value includes the login token.
    /// </summary>
    public string? BaseUrlWithLoginToken { get; init; }
 
    /// <summary>
    /// Gets the Codespaces dashboard URL, if available.
    /// When browser token authentication is enabled, this value includes the login token.
    /// </summary>
    public string? CodespacesUrlWithLoginToken { get; init; }
}
 
/// <summary>
/// Envelope for publishing activities sent over the backchannel.
/// </summary>
internal sealed class PublishingActivity
{
    /// <summary>
    /// Gets the type discriminator for the publishing activity.
    /// </summary>
    public required string Type { get; init; }
 
    /// <summary>
    /// Gets the data containing all properties for the publishing activity.
    /// </summary>
    public required PublishingActivityData Data { get; init; }
}
 
/// <summary>
/// Common data for all publishing activities.
/// </summary>
internal sealed class PublishingActivityData
{
    /// <summary>
    /// Gets the unique identifier for the publishing activity.
    /// </summary>
    public required string Id { get; init; }
 
    /// <summary>
    /// Gets the status text describing the publishing activity.
    /// </summary>
    public required string StatusText { get; init; }
 
    /// <summary>
    /// Gets the completion state of the publishing activity.
    /// </summary>
    public string CompletionState { get; init; } = CompletionStates.InProgress;
 
    /// <summary>
    /// Gets a value indicating whether the publishing activity is complete.
    /// </summary>
    public bool IsComplete => CompletionState is not CompletionStates.InProgress;
 
    /// <summary>
    /// Gets a value indicating whether the publishing activity encountered an error.
    /// </summary>
    public bool IsError => CompletionState is CompletionStates.CompletedWithError;
 
    /// <summary>
    /// Gets a value indicating whether the publishing activity completed with warnings.
    /// </summary>
    public bool IsWarning => CompletionState is CompletionStates.CompletedWithWarning;
 
    /// <summary>
    /// Gets the identifier of the step this task belongs to (only applicable for tasks).
    /// </summary>
    public string? StepId { get; init; }
 
    /// <summary>
    /// Gets the identifier of the parent step used for hierarchical step summaries.
    /// </summary>
    public string? ParentStepId { get; init; }
 
    /// <summary>
    /// Gets the hierarchical level of the step used for display purposes.
    /// Nullable for backwards compatibility with older app hosts that do not send hierarchy metadata.
    /// </summary>
    public int? HierarchyLevel { get; init; }
 
    /// <summary>
    /// Gets the optional completion message for tasks (appears as dimmed child text).
    /// </summary>
    public string? CompletionMessage { get; init; }
 
    /// <summary>
    /// Gets the pipeline summary information to display after pipeline completion.
    /// Each item carries its own key, value, and Markdown formatting flag.
    /// The list preserves the order items were added.
    /// </summary>
    public IReadOnlyList<BackchannelPipelineSummaryItem>? PipelineSummary { get; init; }
 
    /// <summary>
    /// Gets the input information for prompt activities, if available.
    /// </summary>
    public IReadOnlyList<PublishingPromptInput>? Inputs { get; init; }
 
    /// <summary>
    /// Gets the log level for log activities, if available.
    /// </summary>
    public string? LogLevel { get; init; }
 
    /// <summary>
    /// Gets the timestamp for log activities, if available.
    /// </summary>
    public DateTimeOffset? Timestamp { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether markdown formatting is enabled for the publishing activity.
    /// </summary>
    public bool EnableMarkdown { get; init; } = true;
}
 
/// <summary>
/// Represents a single item in a pipeline summary for backchannel transport.
/// </summary>
internal sealed class BackchannelPipelineSummaryItem
{
    /// <summary>
    /// Gets the key or label for the summary item.
    /// </summary>
    public required string Key { get; init; }
 
    /// <summary>
    /// Gets the string value for the summary item.
    /// </summary>
    public required string Value { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether the value contains Markdown formatting.
    /// </summary>
    public bool EnableMarkdown { get; init; }
}
 
/// <summary>
/// Represents an input for a publishing prompt.
/// </summary>
internal sealed class PublishingPromptInput
{
    /// <summary>
    /// Gets the name for the input.
    /// Nullable for backwards compatibility with Aspire 9.5 and older app hosts.
    /// </summary>
    public string? Name { get; init; }
 
    /// <summary>
    /// Gets the label for the input.
    /// </summary>
    public required string Label { get; init; }
 
    /// <summary>
    /// Gets the type of the input.
    /// </summary>
    public required string InputType { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether the input is required.
    /// </summary>
    public bool Required { get; init; }
 
    /// <summary>
    /// Gets the options for the input. Only used by select inputs.
    /// </summary>
    public IReadOnlyList<KeyValuePair<string, string>>? Options { get; init; }
 
    /// <summary>
    /// Gets the default value for the input.
    /// </summary>
    public string? Value { get; init; }
 
    /// <summary>
    /// Gets the validation errors for the input.
    /// </summary>
    public IReadOnlyList<string>? ValidationErrors { get; init; }
 
    /// <summary>
    /// Gets or sets a value indicating whether a custom choice is allowed.
    /// </summary>
    public bool AllowCustomChoice { get; init; }
 
    /// <summary>
    /// Gets or sets a value indicating whether the state should be updated when the input value changes.
    /// </summary>
    public bool UpdateStateOnChange { get; init; }
 
    public bool Loading { get; init; }
 
    public bool Disabled { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether multiple files can be selected for File inputs.
    /// </summary>
    public bool AllowMultipleFiles { get; init; }
 
    /// <summary>
    /// Gets 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>
    /// Gets the maximum file size in bytes for File inputs. Null means no explicit limit.
    /// </summary>
    public long? MaxFileSize { get; init; }
}
 
/// <summary>
/// Constants for publishing activity types.
/// </summary>
internal static class PublishingActivityTypes
{
    public const string Step = "step";
    public const string Task = "task";
    public const string PublishComplete = "publish-complete";
    public const string Prompt = "prompt";
    public const string Log = "log";
}
 
/// <summary>
/// Constants for completion state values.
/// </summary>
internal static class CompletionStates
{
    public const string InProgress = "InProgress";
    public const string Completed = "Completed";
    public const string CompletedWithWarning = "CompletedWithWarning";
    public const string CompletedWithError = "CompletedWithError";
}
 
internal class BackchannelLogEntry
{
    public long SequenceNumber { get; set; }
    public Guid GenerationId { get; set; }
    public required EventId EventId { get; set; }
    public required LogLevel LogLevel { get; set; }
    public required string Message { get; set; }
 
    public string? Exception { get; set; }
    public required DateTimeOffset Timestamp { get; set; }
    public required string CategoryName { get; set; }
}
 
internal class PublishingPromptInputAnswer
{
    public string? Name { get; set; }
    public string? Value { get; set; }
}
 
internal sealed class UploadFileRequest
{
    public required byte[] Data { get; set; }
    public required string FileName { get; set; }
    public required int InteractionId { get; set; }
    public required string InputName { get; set; }
}
 
internal sealed class UploadFileResponse
{
    public required string FileId { get; set; }
}
 
/// <summary>
/// Represents a file reference as serialized in interaction input values.
/// Matches the JSON format: [{"Id":"...","Name":"..."}]
/// </summary>
internal sealed class FileReferenceDto
{
    public required string Id { get; set; }
    public required string Name { get; set; }
}
 
/// <summary>
/// Represents metadata about a pipeline step for display purposes (e.g., --list-steps).
/// </summary>
internal sealed class PipelineStepInfo
{
    /// <summary>
    /// Gets the unique name of the step.
    /// </summary>
    public required string Name { get; init; }
 
    /// <summary>
    /// Gets the description of the step.
    /// </summary>
    public string? Description { get; init; }
 
    /// <summary>
    /// Gets the names of steps that this step depends on.
    /// </summary>
    public string[] DependsOn { get; init; } = [];
 
    /// <summary>
    /// Gets the tags that categorize this step.
    /// </summary>
    public string[] Tags { get; init; } = [];
 
    /// <summary>
    /// Gets the name of the resource this step is associated with, if any.
    /// </summary>
    public string? ResourceName { get; init; }
}
 
/// <summary>
/// Request for getting pipeline step metadata.
/// </summary>
internal sealed class GetPipelineStepsRequest : BackchannelRequest
{
    /// <summary>
    /// Gets or sets the target step name to filter to (including transitive dependencies).
    /// When null, all steps are returned.
    /// </summary>
    public string? Step { get; init; }
 
    /// <inheritdoc />
    public override GetPipelineStepsRequest WithTraceContext(BackchannelTraceContext traceContext) => new()
    {
        TraceContext = traceContext,
        Step = Step
    };
}
 
/// <summary>
/// Response containing pipeline step metadata.
/// </summary>
internal sealed class GetPipelineStepsResponse
{
    /// <summary>
    /// Gets the pipeline steps in topological (execution) order.
    /// </summary>
    public required PipelineStepInfo[] Steps { get; init; }
}
 
/// <summary>
/// Represents the connection information for the Dashboard MCP server.
/// </summary>
internal sealed class DashboardMcpConnectionInfo
{
    /// <summary>
    /// Gets or sets the endpoint URL for the Dashboard MCP server.
    /// </summary>
    public required string EndpointUrl { get; init; }
 
    /// <summary>
    /// Gets or sets the API token for authenticating with the Dashboard MCP server.
    /// </summary>
    public required string ApiToken { get; init; }
}
 
/// <summary>
/// Represents a snapshot of a resource in the application model, suitable for RPC communication.
/// Designed to be extensible - new fields can be added without breaking existing consumers.
/// </summary>
[DebuggerDisplay("Name = {Name}, ResourceType = {ResourceType}, State = {State}, Properties = {Properties.Count}")]
internal sealed class ResourceSnapshot
{
    /// <summary>
    /// Gets the unique name of the resource.
    /// </summary>
    public required string Name { get; init; }
 
    /// <summary>
    /// Gets the monotonically increasing version of this resource snapshot, or <c>0</c> when unavailable.
    /// </summary>
    public long Version { get; init; }
 
    /// <summary>
    /// Gets the display name of the resource.
    /// </summary>
    public string? DisplayName { get; init; }
 
    // ResourceType can't be required because older versions of the backchannel may not set it.
    /// <summary>
    /// Gets the type of the resource (e.g., "Project", "Container", "Executable").
    /// </summary>
    public string? ResourceType { get; init; }
 
    /// <summary>
    /// Gets the type of the resource (e.g., "Project", "Container", "Executable").
    /// </summary>
    [Obsolete("Use ResourceType property instead.")]
    public string? Type
    {
        get => ResourceType;
        init => ResourceType = value;
    }
 
    /// <summary>
    /// Gets the current state of the resource (e.g., "Running", "Stopped", "Starting").
    /// </summary>
    public string? State { get; init; }
 
    /// <summary>
    /// Gets the names of resources this resource is waiting for.
    /// </summary>
    public string[]? WaitingFor { get; init; }
 
    /// <summary>
    /// Gets the state style hint (e.g., "success", "error", "warning").
    /// </summary>
    public string? StateStyle { get; init; }
 
    /// <summary>
    /// Gets the health status of the resource (e.g., "Healthy", "Unhealthy", "Degraded").
    /// </summary>
    public string? HealthStatus { get; init; }
 
    /// <summary>
    /// Gets the exit code if the resource has exited.
    /// </summary>
    public int? ExitCode { get; init; }
 
    /// <summary>
    /// Gets the creation timestamp of the resource.
    /// </summary>
    public DateTimeOffset? CreatedAt { get; init; }
 
    /// <summary>
    /// Gets the start timestamp of the resource.
    /// </summary>
    public DateTimeOffset? StartedAt { get; init; }
 
    /// <summary>
    /// Gets the stop timestamp of the resource.
    /// </summary>
    public DateTimeOffset? StoppedAt { get; init; }
 
    /// <summary>
    /// Gets the URLs exposed by this resource.
    /// </summary>
    public ResourceSnapshotUrl[] Urls { get; init; } = [];
 
    /// <summary>
    /// Gets the relationships to other resources.
    /// </summary>
    public ResourceSnapshotRelationship[] Relationships { get; init; } = [];
 
    /// <summary>
    /// Gets the health reports for this resource.
    /// </summary>
    public ResourceSnapshotHealthReport[] HealthReports { get; init; } = [];
 
    /// <summary>
    /// Gets the volumes mounted to this resource.
    /// </summary>
    public ResourceSnapshotVolume[] Volumes { get; init; } = [];
 
    /// <summary>
    /// Gets the environment variables for this resource.
    /// </summary>
    public ResourceSnapshotEnvironmentVariable[] EnvironmentVariables { get; init; } = [];
 
    /// <summary>
    /// Gets additional properties as key-value pairs.
    /// This allows for extensibility without changing the schema.
    /// </summary>
    public Dictionary<string, JsonNode?> Properties { get; init; } = [];
 
    /// <summary>
    /// Gets a value indicating whether this resource is hidden.
    /// </summary>
    public bool IsHidden { get; init; }
 
    /// <summary>
    /// Gets the MCP server information if the resource exposes an MCP endpoint.
    /// </summary>
    public ResourceSnapshotMcpServer? McpServer { get; init; }
 
    /// <summary>
    /// Gets the commands available for this resource.
    /// </summary>
    public ResourceSnapshotCommand[] Commands { get; init; } = [];
}
 
/// <summary>
/// Represents a command available for a resource.
/// </summary>
[DebuggerDisplay("Name = {Name}, State = {State}")]
internal sealed class ResourceSnapshotCommand
{
    /// <summary>
    /// Gets the command name (e.g., "start", "stop", "restart").
    /// </summary>
    public required string Name { get; init; }
 
    /// <summary>
    /// Gets the display name of the command.
    /// </summary>
    public string? DisplayName { get; init; }
 
    /// <summary>
    /// Gets the description of the command.
    /// </summary>
    public string? Description { get; init; }
 
    /// <summary>
    /// Gets the ordered inputs that describe the invocation arguments accepted by the command.
    /// </summary>
    public ResourceSnapshotCommandArgument[] ArgumentInputs { get; init; } = [];
 
    /// <summary>
    /// Gets where the command is visible to users and clients.
    /// </summary>
    public string Visibility { get; init; } = KnownCommandVisibility.Default;
 
    /// <summary>
    /// Gets the state of the command (e.g., "Enabled", "Disabled", "Hidden").
    /// </summary>
    public required string State { get; init; }
}
 
/// <summary>
/// Represents an invocation argument accepted by a resource command.
/// </summary>
internal sealed class ResourceSnapshotCommandArgument
{
    /// <summary>
    /// Gets the argument name.
    /// </summary>
    public required string Name { get; init; }
 
    /// <summary>
    /// Gets the display label.
    /// </summary>
    public string? Label { get; init; }
 
    /// <summary>
    /// Gets the argument description.
    /// </summary>
    public string? Description { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether the description should be rendered as Markdown.
    /// </summary>
    public bool EnableDescriptionMarkdown { get; init; }
 
    /// <summary>
    /// Gets the input type.
    /// </summary>
    public required string InputType { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether the argument is required.
    /// </summary>
    public bool Required { get; init; }
 
    /// <summary>
    /// Gets the placeholder text.
    /// </summary>
    public string? Placeholder { get; init; }
 
    /// <summary>
    /// Gets the default or submitted value.
    /// </summary>
    public string? Value { get; init; }
 
    /// <summary>
    /// Gets choice options keyed by submitted value.
    /// </summary>
    public Dictionary<string, string?>? Options { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether custom choices are allowed.
    /// </summary>
    public bool AllowCustomChoice { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether the argument input is disabled.
    /// </summary>
    public bool Disabled { get; init; }
 
    /// <summary>
    /// Gets the maximum length for text inputs.
    /// </summary>
    public int? MaxLength { get; init; }
 
    /// <summary>
    /// Gets metadata describing dynamic input loading behavior.
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public ResourceSnapshotCommandArgumentDynamicLoading? DynamicLoading { get; init; }
}
 
/// <summary>
/// Represents dynamic loading metadata for a resource command argument.
/// </summary>
internal sealed class ResourceSnapshotCommandArgumentDynamicLoading
{
    /// <summary>
    /// Gets a value indicating whether the input should always load when prompting starts.
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public bool AlwaysLoadOnStart { get; init; }
 
    /// <summary>
    /// Gets the input names that trigger reloading when their values change.
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string[]? DependsOnInputs { get; init; }
}
 
/// <summary>
/// Represents a URL exposed by a resource.
/// </summary>
[DebuggerDisplay("Name = {Name}, Url = {Url}")]
internal sealed class ResourceSnapshotUrl
{
    /// <summary>
    /// Gets the URL name (e.g., "http", "https", "tcp").
    /// </summary>
    public required string Name { get; init; }
 
    /// <summary>
    /// Gets the full URL including scheme, host, and port.
    /// </summary>
    public required string Url { get; init; }
 
    /// <summary>
    /// Gets whether this is an internal URL.
    /// </summary>
    public bool IsInternal { get; init; }
 
    /// <summary>
    /// Gets the display properties for the URL.
    /// </summary>
    public ResourceSnapshotUrlDisplayProperties? DisplayProperties { get; init; }
}
 
/// <summary>
/// Represents display properties for a URL.
/// </summary>
[DebuggerDisplay("DisplayName = {DisplayName}, SortOrder = {SortOrder}")]
internal sealed class ResourceSnapshotUrlDisplayProperties
{
    /// <summary>
    /// Gets the display name of the URL.
    /// </summary>
    public string? DisplayName { get; init; }
 
    /// <summary>
    /// Gets the sort order for display. Higher numbers are displayed first.
    /// </summary>
    public int SortOrder { get; init; }
}
 
/// <summary>
/// Represents a relationship to another resource.
/// </summary>
[DebuggerDisplay("ResourceName = {ResourceName}, Type = {Type}")]
internal sealed class ResourceSnapshotRelationship
{
    /// <summary>
    /// Gets the name of the related resource.
    /// </summary>
    public required string ResourceName { get; init; }
 
    /// <summary>
    /// Gets the relationship type (e.g., "Parent", "Reference").
    /// </summary>
    public required string Type { get; init; }
}
 
/// <summary>
/// Represents a health report for a resource.
/// </summary>
[DebuggerDisplay("Name = {Name}, Status = {Status}")]
internal sealed class ResourceSnapshotHealthReport
{
    /// <summary>
    /// Gets the name of the health check.
    /// </summary>
    public required string Name { get; init; }
 
    /// <summary>
    /// Gets the status (e.g., "Healthy", "Unhealthy", "Degraded").
    /// </summary>
    public string? Status { get; init; }
 
    /// <summary>
    /// Gets the description of the health report.
    /// </summary>
    public string? Description { get; init; }
 
    /// <summary>
    /// Gets the exception text if the health check failed.
    /// </summary>
    public string? ExceptionText { get; init; }
}
 
/// <summary>
/// Represents a volume mounted to a resource.
/// </summary>
[DebuggerDisplay("Source = {Source}, Target = {Target}")]
internal sealed class ResourceSnapshotVolume
{
    /// <summary>
    /// Gets the source path or volume name.
    /// </summary>
    public string? Source { get; init; }
 
    /// <summary>
    /// Gets the target path in the container.
    /// </summary>
    public required string Target { get; init; }
 
    /// <summary>
    /// Gets the mount type (e.g., "bind", "volume").
    /// </summary>
    public required string MountType { get; init; }
 
    /// <summary>
    /// Gets whether the volume is read-only.
    /// </summary>
    public bool IsReadOnly { get; init; }
}
 
/// <summary>
/// Represents an environment variable for a resource.
/// </summary>
[DebuggerDisplay("Name = {Name}, Value = {Value}")]
internal sealed class ResourceSnapshotEnvironmentVariable
{
    /// <summary>
    /// Gets the name of the environment variable.
    /// </summary>
    public required string Name { get; init; }
 
    /// <summary>
    /// Gets the value of the environment variable.
    /// </summary>
    public string? Value { get; init; }
 
    /// <summary>
    /// Gets whether this environment variable is from the resource specification.
    /// </summary>
    public bool IsFromSpec { get; init; }
}
 
/// <summary>
/// Represents MCP server information for a resource.
/// </summary>
[DebuggerDisplay("EndpointUrl = {EndpointUrl}")]
internal sealed class ResourceSnapshotMcpServer
{
    /// <summary>
    /// Gets the MCP endpoint URL.
    /// </summary>
    public required string EndpointUrl { get; init; }
 
    /// <summary>
    /// Gets the tools exposed by the MCP server.
    /// </summary>
    public required Tool[] Tools { get; init; }
}
 
/// <summary>
/// Represents information about the AppHost for the MCP server.
/// </summary>
internal sealed class AppHostInformation
{
    /// <summary>
    /// Gets or sets the fully qualified path to the AppHost project.
    /// </summary>
    public required string AppHostPath { get; init; }
 
    /// <summary>
    /// Gets or sets the process ID of the AppHost.
    /// </summary>
    public required int ProcessId { get; init; }
 
    /// <summary>
    /// Gets or sets the process ID of the CLI that launched the AppHost, if applicable.
    /// This value is only set when the AppHost is launched via the Aspire CLI.
    /// </summary>
    public int? CliProcessId { get; init; }
 
    /// <summary>
    /// Gets or sets when the AppHost process started using the legacy <see cref="Process.StartTime"/> clock domain.
    /// </summary>
    /// <remarks>
    /// Released AppHosts only report this field. On Linux it can differ from the stable PID-identity
    /// value by an adjacent Unix second, so callers that verify process identity should prefer
    /// <see cref="StableStartedAt"/> when it is present and use a runtime-start-time verifier for this
    /// fallback field.
    /// </remarks>
    public DateTimeOffset? StartedAt { get; init; }
 
    /// <summary>
    /// Gets or sets when the AppHost process started using the stable PID-identity clock domain.
    /// </summary>
    /// <remarks>
    /// Current AppHosts report this additive field so callers can perform exact PID-reuse checks while
    /// still accepting older AppHosts that only sent <see cref="StartedAt"/>.
    /// </remarks>
    public DateTimeOffset? StableStartedAt { get; init; }
 
    /// <summary>
    /// Gets or sets when the CLI process that launched the AppHost started.
    /// This value is only set when the AppHost is launched via the Aspire CLI.
    /// </summary>
    /// <remarks>
    /// This value comes from <c>ASPIRE_CLI_STARTED</c>, which is intentionally stamped from
    /// <see cref="Process.StartTime"/> for released-AppHost compatibility. On Linux it drifts across
    /// processes after a wall-clock adjustment, so prefer <see cref="CliStableStartedAt"/> when it is
    /// present.
    /// </remarks>
    public DateTimeOffset? CliStartedAt { get; init; }
 
    /// <summary>
    /// Gets or sets when the CLI process that launched the AppHost started, using the stable
    /// PID-identity clock domain.
    /// </summary>
    /// <remarks>
    /// This value comes from <c>ASPIRE_CLI_STARTED_STABLE</c> and, unlike <see cref="CliStartedAt"/>,
    /// is derived from Linux <c>/proc</c> start ticks so it survives wall-clock adjustments and can be
    /// compared exactly to guard against CLI PID reuse. Current CLIs stamp it; older CLIs do not, so
    /// this additive field is <see langword="null"/> when the AppHost was launched by an older CLI.
    /// </remarks>
    public DateTimeOffset? CliStableStartedAt { get; init; }
 
    /// <summary>
    /// Gets or sets the log file path of the CLI process that launched the AppHost.
    /// This value is only set when the AppHost is launched via the Aspire CLI.
    /// </summary>
    public string? CliLogFilePath { get; init; }
}
 
/// <summary>
/// Represents a log line from a resource's console output.
/// </summary>
internal sealed class ResourceLogLine
{
    /// <summary>
    /// Gets the name of the resource that produced this log line.
    /// </summary>
    public required string ResourceName { get; init; }
 
    /// <summary>
    /// Gets the line number within the log stream.
    /// </summary>
    public required int LineNumber { get; init; }
 
    /// <summary>
    /// Gets the content of the log line.
    /// </summary>
    public required string Content { get; init; }
 
    /// <summary>
    /// Gets whether this log line is from stderr (error output).
    /// </summary>
    public bool IsError { get; init; }
}
 
#region Terminal
 
/// <summary>
/// Request for getting terminal information for a resource.
/// </summary>
internal sealed class GetTerminalInfoRequest : BackchannelRequest
{
    /// <summary>
    /// Gets the resource name.
    /// </summary>
    public required string ResourceName { get; init; }
 
    /// <inheritdoc />
    public override GetTerminalInfoRequest WithTraceContext(BackchannelTraceContext traceContext)
        => new() { ResourceName = ResourceName, TraceContext = traceContext };
}
 
/// <summary>
/// Per-replica endpoint information for an interactive terminal session. One entry per
/// replica of a resource configured with WithTerminal. Returned inside
/// <see cref="GetTerminalInfoResponse.Replicas"/> when the auxiliary backchannel reports
/// the <see cref="AuxiliaryBackchannelCapabilities.Terminals_V1"/> capability.
/// </summary>
internal sealed class TerminalReplicaInfo
{
    /// <summary>
    /// Gets the zero-based replica index. Stable across the lifetime of the AppHost run.
    /// </summary>
    public required int ReplicaIndex { get; init; }
 
    /// <summary>
    /// Gets a short human-readable label for the replica, suitable for selection prompts and logs.
    /// </summary>
    public required string Label { get; init; }
 
    /// <summary>
    /// Gets the consumer-side Unix domain socket path that viewers (Dashboard, CLI) connect to in
    /// order to attach to this replica's PTY via Hex1b's HMP v1 protocol.
    /// </summary>
    public required string ConsumerUdsPath { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether this replica's upstream producer is currently attached.
    /// True while the underlying PTY is actively delivering bytes to the host. False transiently
    /// between automatic recycles (when DCP relaunches the underlying process and rebinds), and
    /// permanently when the host is shutting down. Identical in meaning to
    /// <see cref="ProducerConnected"/>; both are populated for backwards compatibility with
    /// older clients that branched on this name.
    /// </summary>
    public required bool IsAlive { get; init; }
 
    /// <summary>
    /// Gets the exit code from the most recently-completed producer cycle for this replica,
    /// or null if no cycle has completed yet. Updates each time the upstream producer
    /// disconnects.
    /// </summary>
    public int? ExitCode { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether the upstream producer is currently attached to this
    /// replica. Synonym for <see cref="IsAlive"/> with clearer naming.
    /// </summary>
    public bool ProducerConnected { get; init; }
 
    /// <summary>
    /// Gets the number of completed producer cycles for this replica. Increments each time
    /// the upstream producer disconnects and the host rebinds. Useful as a diagnostic — an
    /// unexpectedly high count indicates the upstream process is crashing repeatedly.
    /// </summary>
    public int RestartCount { get; init; }
 
    /// <summary>
    /// Gets the current terminal grid width in columns, as last negotiated by the active HMP1
    /// primary peer. Falls back to <see cref="GetTerminalInfoResponse.Columns"/> when no peer has
    /// driven a resize yet. Null when the AppHost predates the
    /// <see cref="AuxiliaryBackchannelCapabilities.Terminals_V1"/> capability.
    /// </summary>
    public int? CurrentColumns { get; init; }
 
    /// <summary>
    /// Gets the current terminal grid height in rows. See <see cref="CurrentColumns"/>.
    /// </summary>
    public int? CurrentRows { get; init; }
 
    /// <summary>
    /// Gets the count of HMP1 viewer peers currently attached to this replica's consumer UDS
    /// (Dashboard tabs, CLI <c>aspire terminal attach</c> sessions, etc.). Zero when no viewer
    /// is attached. Null when the AppHost predates the
    /// <see cref="AuxiliaryBackchannelCapabilities.Terminals_V1"/> capability.
    /// </summary>
    public int? AttachedPeerCount { get; init; }
 
    /// <summary>
    /// Gets per-peer details for currently-attached HMP1 viewers, in connect order. Useful for
    /// "who's attached?" diagnostics in <c>aspire terminal ps -v</c>. Null when the AppHost
    /// predates the <see cref="AuxiliaryBackchannelCapabilities.Terminals_V1"/> capability.
    /// </summary>
    public TerminalPeerInfo[]? Peers { get; init; }
}
 
/// <summary>
/// Per-peer identification for an HMP1 client currently attached to a replica's consumer UDS.
/// Mirrors the host-side peer info reported by the terminal host control protocol.
/// </summary>
internal sealed class TerminalPeerInfo
{
    /// <summary>
    /// Gets the HMP1-assigned stable peer identifier for the lifetime of the connection.
    /// </summary>
    public required string PeerId { get; init; }
 
    /// <summary>
    /// Gets the free-form display label the peer reported in its ClientHello, or null if the
    /// peer didn't supply one (e.g. <c>aspire-cli:1234</c>, <c>dashboard:abc12345</c>).
    /// </summary>
    public string? DisplayName { get; init; }
}
 
/// <summary>
/// Response containing terminal information for a resource.
/// </summary>
internal sealed class GetTerminalInfoResponse
{
    /// <summary>
    /// Gets whether terminal access is available for this resource.
    /// </summary>
    public required bool IsAvailable { get; init; }
 
    /// <summary>
    /// Gets the per-replica endpoint information when <see cref="IsAvailable"/> is true.
    /// Null for older AppHosts that predate the
    /// <see cref="AuxiliaryBackchannelCapabilities.Terminals_V1"/> capability.
    /// </summary>
    public TerminalReplicaInfo[]? Replicas { get; init; }
 
    /// <summary>
    /// Gets the legacy single-socket UDS path. Always null in 13.4+; preserved on the wire
    /// so older CLI builds that deserialize this response keep working without crashing on an
    /// unexpected schema.
    /// </summary>
    public string? SocketPath { get; init; }
 
    /// <summary>
    /// Gets the AppHost-configured initial terminal width in columns. Hint only; viewers may
    /// negotiate a different size after attaching.
    /// </summary>
    public int Columns { get; init; }
 
    /// <summary>
    /// Gets the AppHost-configured initial terminal height in rows. Hint only; viewers may
    /// negotiate a different size after attaching.
    /// </summary>
    public int Rows { get; init; }
}
 
/// <summary>
/// Request for listing every <c>WithTerminal</c>-enabled resource. Empty payload — the AppHost
/// already knows which resources have a <c>TerminalAnnotation</c>. Gated on the
/// <see cref="AuxiliaryBackchannelCapabilities.Terminals_V1"/> capability.
/// </summary>
internal sealed class ListTerminalsRequest : BackchannelRequest
{
    /// <inheritdoc />
    public override ListTerminalsRequest WithTraceContext(BackchannelTraceContext traceContext)
        => new() { TraceContext = traceContext };
}
 
/// <summary>
/// One entry per <c>WithTerminal</c>-enabled resource. Returned inside
/// <see cref="ListTerminalsResponse.Terminals"/>. Replica details (current size, attached peers)
/// are only populated when the host process is reachable; otherwise <see cref="IsHostReachable"/>
/// is false and the per-replica entries are degraded (<see cref="TerminalReplicaInfo.IsAlive"/> =
/// false, AppHost-known <see cref="TerminalReplicaInfo.ConsumerUdsPath"/>), but the array shape
/// stays consistent so diagnostics stay legible.
/// </summary>
internal sealed class TerminalSummary
{
    /// <summary>
    /// Gets the resource name (matches <c>IResource.Name</c>).
    /// </summary>
    public required string ResourceName { get; init; }
 
    /// <summary>
    /// Gets a short human-readable display name. Today identical to <see cref="ResourceName"/>;
    /// kept separate so the AppHost can substitute a friendlier name later (e.g. when a resource
    /// has a display name annotation).
    /// </summary>
    public required string DisplayName { get; init; }
 
    /// <summary>
    /// Gets the AppHost-configured initial terminal width. Falls back when no replica has reported
    /// a resize.
    /// </summary>
    public required int ConfiguredColumns { get; init; }
 
    /// <summary>
    /// Gets the AppHost-configured initial terminal height.
    /// </summary>
    public required int ConfiguredRows { get; init; }
 
    /// <summary>
    /// Gets a value indicating whether the terminal host process for this resource was
    /// reachable when the snapshot was taken. False when the host hasn't started yet, the control
    /// UDS isn't bound, or the control RPC timed out.
    /// </summary>
    public required bool IsHostReachable { get; init; }
 
    /// <summary>
    /// Gets the per-replica details. One entry per configured replica, in replica index order.
    /// Replicas whose host wasn't reachable when the snapshot was taken still appear here with
    /// <see cref="TerminalReplicaInfo.IsAlive"/> = false and the AppHost-known
    /// <see cref="TerminalReplicaInfo.ConsumerUdsPath"/> populated, so the diagnostic shape
    /// stays consistent regardless of host reachability. Null only when the producer didn't
    /// supply any replica info (older AppHost predating per-replica fan-out).
    /// </summary>
    public TerminalReplicaInfo[]? Replicas { get; init; }
}
 
/// <summary>
/// Response from <c>ListTerminalsAsync</c>. Lists every <c>WithTerminal</c>-enabled resource in the
/// AppHost. Empty array when no resource is configured for terminals.
/// </summary>
internal sealed class ListTerminalsResponse
{
    /// <summary>
    /// Gets the per-resource summaries. Empty (not null) when there are no terminal-enabled resources.
    /// </summary>
    public required TerminalSummary[] Terminals { get; init; }
}
 
#endregion
/// <summary>
/// Represents a batch of resource console log lines.
/// </summary>
internal sealed class ResourceLogBatch
{
    /// <summary>
    /// Gets the log lines in this batch.
    /// </summary>
    public required ResourceLogLine[] Lines { get; init; }
}