// 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; }
}