Skip to content

Use Spectre.Console link markup for CLI URL output#15573

Open
JamesNK wants to merge 4 commits intomainfrom
cli-link-markup-urls
Open

Use Spectre.Console link markup for CLI URL output#15573
JamesNK wants to merge 4 commits intomainfrom
cli-link-markup-urls

Conversation

@JamesNK
Copy link
Member

@JamesNK JamesNK commented Mar 25, 2026

Description

Use Spectre.Console [link] markup for all CLI URL output so terminal hyperlinks work correctly and aren't broken by line wrapping.

Changes

  • DescribeCommand: Endpoint URLs use [link] markup with DisplayName as link text when present. Endpoints are sorted matching the dashboard order (SortOrder desc, https first, then by Name).
  • PsCommand: Dashboard URL uses [link] markup with scheme://authority as display text (hides the login token path/querystring).
  • DoctorCommand: Prerequisites URL and environment check result links use [link] markup. URL extracted from resource string into code.
  • Program.cs: Telemetry notice URL uses [link] markup via MarkupLine. URL extracted from resource string into code.
  • Shared KnownUnsupportedUrlSchemes: Extracted the browser-unsupported URL scheme deny-list from ResourceUrlHelpers.cs into a shared file under src/Shared/, now used by both CLI and Dashboard. Added redis and rediss to the list.

Validation

  • All 1708 CLI unit tests pass
  • All 11 Dashboard ResourceUrlHelpers tests pass
  • Both CLI and Dashboard projects compile cleanly

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?
    • Yes
    • No

- Wrap URLs in [link] markup so terminal hyperlinks work and aren't broken by line wrapping
- DescribeCommand: use DisplayName as link text when present, sort endpoints matching dashboard order (SortOrder desc, https first, then by Name)
- PsCommand: dashboard URL uses link markup with scheme://authority as display text
- DoctorCommand: prerequisites URL and environment check links use link markup
- Program.cs: telemetry notice URL uses link markup
- Extract URL scheme deny-list into shared KnownUnsupportedUrlSchemes.cs, used by both CLI and Dashboard
- Add redis and rediss to unsupported URL schemes
Copilot AI review requested due to automatic review settings March 25, 2026 06:43
@github-actions
Copy link
Contributor

github-actions bot commented Mar 25, 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 -- 15573

Or

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

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

Updates Aspire CLI (and Dashboard URL rendering) to use Spectre.Console [link] markup so URLs render as terminal hyperlinks and aren’t broken by wrapping; also centralizes the “unsupported URL scheme” deny-list into a shared helper.

Changes:

  • Added KnownUnsupportedUrlSchemes in src/Shared/ and wired it into Dashboard + CLI.
  • Updated CLI commands (Describe/Ps/Doctor/Program) to emit [link] markup for URLs and to sort/format endpoint lists.
  • Parameterized hard-coded URLs in resource strings to support injecting link markup at runtime.

Reviewed changes

Copilot reviewed 36 out of 38 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/Shared/KnownUnsupportedUrlSchemes.cs New shared deny-list + helper for determining whether a URL should be rendered as a link.
src/Aspire.Dashboard/Model/ResourceUrlHelpers.cs Uses shared unsupported-scheme list when deciding whether to surface URLs as links.
src/Aspire.Dashboard/Aspire.Dashboard.csproj Links the new shared file into the Dashboard build.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.zh-Hant.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.zh-Hans.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.tr.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.ru.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.pt-BR.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.pl.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.ko.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.ja.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.it.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.fr.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.es.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.de.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/RootCommandStrings.cs.xlf Telemetry notice string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hant.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.zh-Hans.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.tr.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ru.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pt-BR.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.pl.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ko.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.ja.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.it.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.fr.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.es.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.de.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/xlf/DoctorCommandStrings.cs.xlf Prerequisites link string updated to accept a URL placeholder.
src/Aspire.Cli/Resources/RootCommandStrings.resx Parameterized telemetry URL and added translator comment.
src/Aspire.Cli/Resources/RootCommandStrings.Designer.cs Regenerated designer docs for the updated resource format string.
src/Aspire.Cli/Resources/DoctorCommandStrings.resx Parameterized prerequisites URL and added translator comment.
src/Aspire.Cli/Resources/DoctorCommandStrings.Designer.cs Regenerated designer docs for the updated resource format string.
src/Aspire.Cli/Program.cs Emits telemetry notice URL as Spectre [link] markup.
src/Aspire.Cli/Commands/PsCommand.cs Renders Dashboard URL as [link] with shortened display text (scheme://authority).
src/Aspire.Cli/Commands/DoctorCommand.cs Uses [link] markup for prerequisites and environment-check links.
src/Aspire.Cli/Commands/DescribeCommand.cs Sorts/formats endpoints and renders them using [link] markup.
src/Aspire.Cli/Aspire.Cli.csproj Links the new shared file into the CLI build.
Files not reviewed (2)
  • src/Aspire.Cli/Resources/DoctorCommandStrings.Designer.cs: Language not supported
  • src/Aspire.Cli/Resources/RootCommandStrings.Designer.cs: Language not supported

Comment on lines +15 to +26
public static readonly HashSet<string> Schemes = new(StringComparer.OrdinalIgnoreCase)
{
"gopher",
"ws",
"wss",
"news",
"nntp",
"telnet",
"tcp",
"redis",
"rediss"
};
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Schemes is a mutable HashSet<string> exposed as a public static readonly field. Even though the containing type is internal, any code in the assembly can still add/remove entries at runtime, which can lead to hard-to-debug behavior changes and thread-safety issues. Consider making the set private/immutable (e.g., FrozenSet<string> or a private HashSet + helper methods like IsUnsupportedScheme / IsLinkableUrl).

Copilot uses AI. Check for mistakes.
Comment on lines +303 to 307
var endpoints = OrderUrls(snapshot.Urls.Where(e => !e.IsInternal))
.Select(e => (e.Url, DisplayName: e.DisplayProperties?.DisplayName ?? ""))
.ToArray();

return new ResourceDisplayState(displayName, snapshot.State, snapshot.HealthStatus, endpoints);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

ResourceDisplayState is used for deduplication via Equals, but Endpoints is now an array. Record equality will compare arrays by reference, so the newly-created ToArray() will almost always make displayState unequal to the previous value even when endpoints didn't change, defeating the deduplication logic. Use a value type with structural equality (e.g., store a normalized string, use ImmutableArray + custom IEquatable, or implement Equals using SequenceEqual).

Copilot uses AI. Check for mistakes.
Comment on lines +218 to +221
if (!string.IsNullOrEmpty(appHost.DashboardUrl) && Uri.TryCreate(appHost.DashboardUrl, UriKind.Absolute, out var dashboardUri))
{
var displayText = $"{dashboardUri.Scheme}://{dashboardUri.Authority}";
dashboard = $"[link={Markup.Escape(appHost.DashboardUrl)}]{Markup.Escape(displayText)}[/]";
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

If appHost.DashboardUrl is non-empty but fails Uri.TryCreate (unexpected format, missing scheme, etc.), the UI now shows - and drops the original value. It would be more robust to fall back to displaying the escaped raw string (without link markup) so users can still see/copy the URL even if it isn't parseable.

Suggested change
if (!string.IsNullOrEmpty(appHost.DashboardUrl) && Uri.TryCreate(appHost.DashboardUrl, UriKind.Absolute, out var dashboardUri))
{
var displayText = $"{dashboardUri.Scheme}://{dashboardUri.Authority}";
dashboard = $"[link={Markup.Escape(appHost.DashboardUrl)}]{Markup.Escape(displayText)}[/]";
if (!string.IsNullOrEmpty(appHost.DashboardUrl))
{
if (Uri.TryCreate(appHost.DashboardUrl, UriKind.Absolute, out var dashboardUri))
{
var displayText = $"{dashboardUri.Scheme}://{dashboardUri.Authority}";
dashboard = $"[link={Markup.Escape(appHost.DashboardUrl)}]{Markup.Escape(displayText)}[/]";
}
else
{
// Fallback: show the raw dashboard URL as plain, escaped text when it cannot be parsed.
dashboard = Markup.Escape(appHost.DashboardUrl);
}

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

🎬 CLI E2E Test Recordings — 49 recordings uploaded (commit 332b668)

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
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
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ 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 #23529440751

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants