Fix describe duplicate rows and colorize resource name in describe#14803
Fix describe duplicate rows and colorize resource name in describe#14803JamesNK merged 4 commits intorelease/13.2from
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14803Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14803" |
🎬 CLI E2E Test RecordingsThe following terminal recordings are available for commit
📹 Recordings uploaded automatically from CI run #22537643658 |
|
Why would we show a stream of resource updates for all running apphosts? Or is that text incorrect? |
There was a problem hiding this comment.
Pull request overview
Updates Aspire CLI describe/resources and logs output to reduce watch-mode noise and make resource name coloring consistent across commands (fixing #14405).
Changes:
- Add per-resource deduplication in
describe --followfor both JSON (NDJSON) and human-readable output. - Introduce a shared
ResourceColorMapand apply resource-name coloring indescribeandlogs. - Adjust CLI test infrastructure to support disabling ANSI/markup rendering for stable assertions.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | Adds test option to disable ANSI and improves writer line buffering to better capture Spectre output. |
| tests/Aspire.Cli.Tests/Commands/LogsCommandTests.cs | Updates tests to disable ANSI via test options instead of NO_COLOR. |
| tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs | Adds follow-mode deduplication tests for JSON and table/text output. |
| src/Aspire.Cli/Utils/ResourceColorMap.cs | New shared utility to assign stable colors per resource name. |
| src/Aspire.Cli/Commands/LogsCommand.cs | Switches to shared color mapping and relies on Spectre handling for ANSI/no-color behavior. |
| src/Aspire.Cli/Commands/DescribeCommand.cs | Adds watch-mode output dedup + colorized names in follow output and tables. |
| // Maintain a list of all resources seen so far for relationship resolution. | ||
| // Seed with a snapshot so that display names resolve correctly from the start. | ||
| var allResources = await connection.GetResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false); | ||
|
|
||
| // Cache the last displayed content per resource to avoid duplicate output. | ||
| // Values are either a string (JSON mode) or a ResourceDisplayState (non-JSON mode). | ||
| var lastDisplayedContent = new Dictionary<string, object>(StringComparers.ResourceName); | ||
|
|
||
| // Stream resource snapshots | ||
| await foreach (var snapshot in connection.WatchResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false)) | ||
| { | ||
| // Update the dictionary with the latest snapshot for this resource | ||
| allResources[snapshot.Name] = snapshot; | ||
|
|
||
| // Filter by resource name if specified | ||
| if (resourceName is not null) | ||
| { | ||
| var resolved = ResourceSnapshotMapper.ResolveResources(resourceName, allResources.Values.ToList()); | ||
| var resolved = ResourceSnapshotMapper.ResolveResources(resourceName, allResources); | ||
| if (!resolved.Any(r => string.Equals(r.Name, snapshot.Name, StringComparison.OrdinalIgnoreCase))) | ||
| { |
There was a problem hiding this comment.
In watch mode, allResources is initialized as the full snapshot list and then reused unchanged. This breaks name resolution and filtering because ResolveResources() and GetResourceName() assume the collection contains one entry per resource (current state), not a growing history. With multiple snapshots for the same resource, ResolveResources() can stop matching on DisplayName, and GetResourceName() can incorrectly think replicas exist and fall back to resource.Name. Consider keeping allResources as a dictionary keyed by snapshot.Name (seeded from GetResourceSnapshotsAsync()), and update/insert on each watched snapshot before calling ResolveResources() / MapToResourceJson() / BuildResourceDisplayState().
| _ => health | ||
| }; | ||
|
|
||
| table.AddRow(displayName, type, stateText, healthText, endpoints); | ||
| table.AddRow($"[{_resourceColorMap.GetColor(displayName)}]{displayName.EscapeMarkup()}[/]", type, stateText, healthText, endpoints); | ||
| } |
There was a problem hiding this comment.
table.AddRow(...) mixes Spectre markup cells (e.g., stateText/healthText and the colorized name) with unescaped dynamic strings (type, endpoints, and the underlying state/health values inside the markup). Because Table.AddRow(string, ...) treats strings as markup, values like IPv6 endpoints (http://[::1]:...) or unexpected characters in state/health can be parsed as markup and throw or render incorrectly. Escape non-markup values (and escape state/health before wrapping them in color tags) when building the row.
The message "showing all running app hosts" is displayed before selecting a running app host. Can improve output by displaying the selected app host. |
d465cd6 to
2d65653
Compare

Description
Fixes #14405
aspire logsaspire logsthat skips ANSI characters. This feature is built into Spectre.ConsoleAfter:

Also add colors to table:

I think colors need a tweak in how they're displayed, but it's a starting point.
Checklist
<remarks />and<code />elements on your triple slash comments?aspire.devissue: