Skip to content

Add command result support for resource commands#15622

Open
davidfowl wants to merge 4 commits intomainfrom
davidfowl/command-results
Open

Add command result support for resource commands#15622
davidfowl wants to merge 4 commits intomainfrom
davidfowl/command-results

Conversation

@davidfowl
Copy link
Contributor

Summary

Adds Result and ResultFormat properties to ExecuteCommandResult, allowing resource commands to return structured output data (text or JSON) back to the caller.

Part of #15610

Changes

Model

  • New CommandResultFormat enum: None, Text, Json
  • ExecuteCommandResult gains Result (string?) and ResultFormat properties
  • CommandResults.Success(result, format) overload for producing results

Proto/gRPC

  • ResourceCommandResponse gets optional string result and CommandResultFormat result_format fields

Backchannel

  • ExecuteResourceCommandResponse gains Result and ResultFormat (string) properties
  • Mapping in AuxiliaryBackchannelRpcTarget

Dashboard

  • ResourceCommandResponseViewModel gains Result and CommandResultFormat
  • On command success with result data, opens TextVisualizerDialog (existing JSON/XML/text viewer with syntax highlighting)
  • JSON results use FixedFormat to lock the viewer to JSON mode

CLI

  • Displays command result with Spectre markup escaping (prevents [/] in JSON from being interpreted as markup)
  • MCP tool returns result as additional TextContentBlock

Replica aggregation

  • Takes the first successful replica's result when multiple replicas return data

Testing

  • 6 new hosting tests (result flow, factory methods, replica aggregation with results)
  • 4 new CLI tests (MCP tool result handling, markup escaping, no-result path)

@github-actions
Copy link
Contributor

github-actions bot commented Mar 26, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15622

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15622"

@afscrome
Copy link
Contributor

Something I've been playing around with in my JWT scenario is writing the output directly to the user's clipboard - something worth thinking about. Works reasonably well for secrets

@davidfowl davidfowl force-pushed the davidfowl/command-results branch from d532f96 to 2f9c523 Compare March 26, 2026 22:13
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl davidfowl force-pushed the davidfowl/command-results branch from 2f9c523 to fc149ed Compare March 26, 2026 23:08
davidfowl and others added 3 commits March 26, 2026 16:48
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl davidfowl marked this pull request as ready for review March 27, 2026 01:19
Copilot AI review requested due to automatic review settings March 27, 2026 01:19
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end support for resource commands to return structured result data (text/JSON) back to callers (Dashboard, CLI, MCP), flowing from hosting model → backchannel/gRPC → UI/CLI.

Changes:

  • Introduces CommandResultFormat and extends ExecuteCommandResult/CommandResults.Success(...) to carry result payload + format.
  • Extends dashboard gRPC/backchannel contracts and mapping to transport result/result_format.
  • Updates Dashboard/CLI/MCP to surface command results, plus adds/updates tests and regenerated polyglot codegen snapshots.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Aspire.Hosting.Tests/ResourceCommandServiceTests.cs Adds hosting tests for result propagation and replica aggregation behavior.
tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts Updates TS generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs Updates Rust generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py Updates Python generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java Updates Java generated API snapshots for new fields/enum.
tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go Updates Go generated API snapshots for new fields/enum.
tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs Adds a hook to capture DisplayMessage calls in CLI tests.
tests/Aspire.Cli.Tests/Mcp/ExecuteResourceCommandToolTests.cs Verifies MCP tool returns additional content block when a command returns result data.
tests/Aspire.Cli.Tests/Commands/ResourceCommandHelperTests.cs Adds CLI tests around displaying command results.
src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto Adds result and result_format fields + CommandResultFormat enum to the dashboard proto.
src/Aspire.Hosting/Dashboard/DashboardServiceData.cs Extends command execution tuple to include result payload + format.
src/Aspire.Hosting/Dashboard/DashboardService.cs Maps hosting command results into gRPC response fields.
src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs Adds backchannel DTO properties for result payload + format.
src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs Maps hosting ExecuteCommandResult to backchannel result fields.
src/Aspire.Hosting/ApplicationModel/ResourceCommandService.cs Aggregates replica results and returns the first successful result payload/format.
src/Aspire.Hosting/ApplicationModel/ResourceCommandAnnotation.cs Adds CommandResultFormat, ExecuteCommandResult.Result/ResultFormat, and CommandResults.Success(...) overload.
src/Aspire.Dashboard/ServiceClient/Partials.cs Maps gRPC result fields into dashboard view model.
src/Aspire.Dashboard/Model/ResourceCommandResponseViewModel.cs Adds result payload + format to the dashboard model and defines CommandResultFormat.
src/Aspire.Dashboard/Model/DashboardCommandExecutor.cs Opens a text visualizer dialog on successful command results (locks to JSON when applicable).
src/Aspire.Cli/Mcp/Tools/ExecuteResourceCommandTool.cs Returns command result as an additional MCP TextContentBlock on success.
src/Aspire.Cli/Commands/ResourceCommandHelper.cs Displays command result in CLI after successful execution.
playground/Stress/Stress.AppHost/Program.cs Adds sample commands returning JSON/text results in the stress playground.

Comment on lines +156 to +162
_ = TextVisualizerDialog.OpenDialogAsync(new OpenTextVisualizerDialogOptions
{
DialogService = serviceProvider.GetRequiredService<DashboardDialogService>(),
ValueDescription = command.GetDisplayName(),
Value = response.Result,
FixedFormat = fixedFormat
});
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TextVisualizerDialog.OpenDialogAsync(...) is an async call that’s currently fire-and-forget (_ = ...) without any exception observation. If ShowDialogAsync throws, the exception can become unobserved and the user won’t see the result dialog. Consider awaiting this call (it should complete once the dialog is shown, not when it’s closed), or explicitly handle/log exceptions if you need it to be non-blocking.

Copilot uses AI. Check for mistakes.

if (response.Result is not null)
{
interactionService.DisplayMessage(KnownEmojis.Information, response.Result.EscapeMarkup());
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IInteractionService.DisplayMessage already escapes Spectre markup when allowMarkup is false (see ConsoleInteractionService.DisplayMessage). Calling EscapeMarkup() here will double-escape the result, producing incorrect output (e.g., [[[[/]]]]). Pass the raw response.Result and rely on DisplayMessage’s built-in escaping (or set allowMarkup: true and escape exactly once if you intend to pre-escape).

Suggested change
interactionService.DisplayMessage(KnownEmojis.Information, response.Result.EscapeMarkup());
interactionService.DisplayMessage(KnownEmojis.Information, response.Result);

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +47
Assert.Equal(0, exitCode);
Assert.NotNull(capturedMessage);
// Verify the brackets are escaped so Spectre doesn't interpret them as markup
// EscapeMarkup doubles [ and ] so [" becomes [["
Assert.Contains("[[", capturedMessage);
Assert.Contains("]]", capturedMessage);
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts that ResourceCommandHelper escapes Spectre markup (expects [[/]]), but in production the escaping is performed by ConsoleInteractionService.DisplayMessage when allowMarkup is false. With the current helper implementation, real output will be double-escaped; if the helper is fixed to stop pre-escaping, this test should be updated to reflect the actual contract (helper passes raw result; interaction service handles escaping), or TestInteractionService.DisplayMessage should mimic the production escaping behavior to catch double-escaping regressions.

Copilot uses AI. Check for mistakes.
Copy link
Member

@JamesNK JamesNK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review


if (response.Result is not null)
{
interactionService.DisplayMessage(KnownEmojis.Information, response.Result.EscapeMarkup());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the method used here should depend on the content result format. JSON (and maybe text as well) should use IInteractionService.DisplayRawText.

And we should consider making the aspire execute resource command in the CLI sending all messages to stderr and only printing the command result to stdout.

message ResourceCommandResponse {
ResourceCommandResponseKind kind = 1;
optional string error_message = 2;
optional string result = 3;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows output to be printed once when the command is finished. What do you think of being able to support streaming result content?

It would be a lot more complicated. We'd need to introduce a new gRPC method for calling resource commands, and it would support streaming result messages. ResourceCommandService would need to be updated to support it. Dashboard would try to use the new method, and if it's not implemented then use the old one. Some other stuff I haven't thought about.

@github-actions
Copy link
Contributor

🎬 CLI E2E Test Recordings — 52 recordings uploaded (commit ef703e1)

View recordings
Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJavaEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateJavaAppHostWithViteApp ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RunWithMissingAwaitShowsHelpfulError ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
TypeScriptAppHostWithProjectReferenceIntegration ▶️ View Recording

📹 Recordings uploaded automatically from CI run #23626358412

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants