Skip to content

Commit 69108a4

Browse files
IEvangelistCopilot
andauthored
Make CLI log paths clickable (#16603)
* Make CLI log paths clickable Render CLI log file paths as terminal hyperlinks while preserving the existing neutral log-reference styling for error output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Make run summary log path clickable Render the aspire run summary log path as a file hyperlink while keeping the existing summary label formatting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Make remaining CLI log paths clickable Address two more sites that emit a log file path but were missed by the initial commits: - DashboardRunCommand.RenderDashboardSummary: the "Logs" row in the `aspire dashboard run` summary now renders as a clickable file link. The method becomes `internal static` and takes `IInteractionService` so it can be tested directly, mirroring the RunCommand pattern. - AppHostLauncher.HandleLaunchFailure: the "Check logs for details: {0}" message references the detached child CLI's log file, not the session's log file, so the auto-linkify path in ConsoleInteractionService does not match it. Pre-format with EscapeMarkupWithFileLink and pass allowMarkup: true so the child log path is also clickable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback for PR #16603 Refactor clickable log path support per JamesNK's review: - Move file-link helper into MarkupHelpers as SafeFileLink (drops the special-cased helpers ConsoleHelpers.EscapeMarkupWithFileLink / FormatPathAsFileLink that only existed for log paths) - Drop the auto-detect log-path machinery in ConsoleInteractionService.DisplayError; callers now opt in by passing allowMarkup: true and formatting the link explicitly with MarkupHelpers.SafeFileLink - Add allowMarkup parameter to IInteractionService.DisplayError so callers control markup interpretation; default false preserves the safe escape-by-default behavior - Drop the trailing 'See logs at {0}' suffix from ProjectCouldNotBeBuilt and ProjectCouldNotBeCreated; callers emit a separate DisplayMessage(SeeLogsAt, SafeFileLink(...)) line so the URL is built at the call site rather than rewriting the rendered string - Document why bracket characters in file paths still need explicit '%5B' / '%5D' percent-encoding (Uri.AbsoluteUri leaves them untouched in the path component, and Spectre.Console's EscapeMarkup only protects the markup parser, not the OSC 8 hyperlink target). Added MarkupHelpersTests.SafeFileLink_* coverage for both the empty-path and bracketed-path cases - Update remaining call sites (RunCommand, NewCommand, InitCommand, DashboardRunCommand, AgentInitCommand, TelemetryCommandHelpers, AppHostLauncher, StartupErrorWriter) to use the new helper - Also fixes a stale '_interactionService' field reference inside the now-static DashboardRunCommand.RenderDashboardSummary method that surfaced after rebasing onto upstream/main Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address hyperlink review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0015b9f commit 69108a4

39 files changed

Lines changed: 416 additions & 106 deletions

src/Aspire.Cli/Commands/AgentInitCommand.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,10 @@ private async Task<AgentInitExecutionResult> ExecuteAgentInitAsync(DirectoryInfo
372372
if (hasErrors)
373373
{
374374
_interactionService.DisplayMessage(KnownEmojis.Warning, AgentCommandStrings.ConfigurationCompletedWithErrors);
375-
_interactionService.DisplayMessage(KnownEmojis.PageFacingUp, string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, ExecutionContext.LogFilePath));
375+
_interactionService.DisplayMessage(
376+
KnownEmojis.PageFacingUp,
377+
string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, MarkupHelpers.SafeFileLink(_interactionService, ExecutionContext.LogFilePath)),
378+
allowMarkup: true);
376379
}
377380
else
378381
{

src/Aspire.Cli/Commands/AppHostLauncher.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,14 @@ private int HandleLaunchFailure(LaunchResult result, string childLogFile)
375375
}
376376
}
377377

378-
interactionService.DisplayMessage(KnownEmojis.MagnifyingGlassTiltedLeft, string.Format(
378+
var checkLogsMessage = string.Format(
379379
CultureInfo.CurrentCulture,
380380
RunCommandStrings.CheckLogsForDetails,
381-
childLogFile));
381+
MarkupHelpers.SafeFileLink(interactionService, childLogFile));
382+
interactionService.DisplayMessage(
383+
KnownEmojis.MagnifyingGlassTiltedLeft,
384+
checkLogsMessage,
385+
allowMarkup: true);
382386

383387
return ExitCodeConstants.FailedToDotnetRunAppHost;
384388
}

src/Aspire.Cli/Commands/DashboardRunCommand.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,9 @@ private static string GetExitCodeMessage(int exitCode)
306306
};
307307
}
308308

309-
private void RenderDashboardSummary(DashboardInfo info, string logFilePath)
309+
internal static void RenderDashboardSummary(IInteractionService interactionService, DashboardInfo info, string logFilePath)
310310
{
311-
_interactionService.DisplayEmptyLine();
311+
interactionService.DisplayEmptyLine();
312312
var grid = new Grid();
313313
grid.AddColumn();
314314
grid.AddColumn();
@@ -326,7 +326,7 @@ private void RenderDashboardSummary(DashboardInfo info, string logFilePath)
326326
// Dashboard row
327327
grid.AddRow(
328328
new Align(new Markup($"[bold green]{dashboardLabel}[/]:"), HorizontalAlignment.Right),
329-
new Markup(MarkupHelpers.SafeLink(_interactionService, info.DashboardUrl)));
329+
new Markup(MarkupHelpers.SafeLink(interactionService, info.DashboardUrl)));
330330
grid.AddRow(Text.Empty, Text.Empty);
331331

332332
// OTLP gRPC row
@@ -344,10 +344,10 @@ private void RenderDashboardSummary(DashboardInfo info, string logFilePath)
344344
// Logs row
345345
grid.AddRow(
346346
new Align(new Markup($"[bold green]{logsLabel}[/]:"), HorizontalAlignment.Right),
347-
new Text(logFilePath));
347+
new Markup(MarkupHelpers.SafeFileLink(interactionService, logFilePath)));
348348

349349
var padder = new Padder(grid, new Padding(3, 0));
350-
_interactionService.DisplayRenderable(padder);
350+
interactionService.DisplayRenderable(padder);
351351
}
352352

353353
private async Task<int> ExecuteForegroundAsync(string managedPath, List<string> dashboardArgs, DashboardInfo dashboardInfo, IDictionary<string, string>? environmentVariables, CancellationToken cancellationToken)
@@ -446,7 +446,10 @@ private async Task<int> ExecuteForegroundAsync(string managedPath, List<string>
446446
: DashboardCommandStrings.DashboardStartTimedOut;
447447

448448
_interactionService.DisplayError(exitMessage);
449-
_interactionService.DisplayMessage(KnownEmojis.PageFacingUp, string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, ExecutionContext.LogFilePath));
449+
_interactionService.DisplayMessage(
450+
KnownEmojis.PageFacingUp,
451+
string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, MarkupHelpers.SafeFileLink(_interactionService, ExecutionContext.LogFilePath)),
452+
allowMarkup: true);
450453

451454
if (!process.HasExited)
452455
{
@@ -457,7 +460,7 @@ private async Task<int> ExecuteForegroundAsync(string managedPath, List<string>
457460
}
458461

459462
// Dashboard is ready.
460-
RenderDashboardSummary(dashboardInfo, ExecutionContext.LogFilePath);
463+
RenderDashboardSummary(_interactionService, dashboardInfo, ExecutionContext.LogFilePath);
461464
_interactionService.DisplayEmptyLine();
462465

463466
try

src/Aspire.Cli/Commands/InitCommand.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
132132

133133
if (dropResult != ExitCodeConstants.Success)
134134
{
135-
InteractionService.DisplayError(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ProjectCouldNotBeCreated, ExecutionContext.LogFilePath));
135+
InteractionService.DisplayError(InteractionServiceStrings.ProjectCouldNotBeCreated);
136+
InteractionService.DisplayMessage(
137+
KnownEmojis.PageFacingUp,
138+
string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, MarkupHelpers.SafeFileLink(InteractionService, ExecutionContext.LogFilePath)),
139+
allowMarkup: true);
136140
return dropResult;
137141
}
138142

src/Aspire.Cli/Commands/NewCommand.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,11 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
404404
if (!resolveResult.Success)
405405
{
406406
InteractionService.DisplayError(resolveResult.ErrorMessage);
407-
InteractionService.DisplayError(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ProjectCouldNotBeCreated, ExecutionContext.LogFilePath));
407+
InteractionService.DisplayError(InteractionServiceStrings.ProjectCouldNotBeCreated);
408+
InteractionService.DisplayMessage(
409+
KnownEmojis.PageFacingUp,
410+
string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, MarkupHelpers.SafeFileLink(InteractionService, ExecutionContext.LogFilePath)),
411+
allowMarkup: true);
408412
return ExitCodeConstants.InvalidCommand;
409413
}
410414

src/Aspire.Cli/Commands/RunCommand.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,11 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
290290
{
291291
InteractionService.DisplayLines(outputCollector.GetLines());
292292
}
293-
InteractionService.DisplayError(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ProjectCouldNotBeBuilt, ExecutionContext.LogFilePath));
293+
InteractionService.DisplayError(InteractionServiceStrings.ProjectCouldNotBeBuilt);
294+
InteractionService.DisplayMessage(
295+
KnownEmojis.PageFacingUp,
296+
string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, MarkupHelpers.SafeFileLink(InteractionService, ExecutionContext.LogFilePath)),
297+
allowMarkup: true);
294298
return await pendingRun;
295299
}
296300

@@ -464,7 +468,10 @@ await InteractionService.DisplayLiveAsync(BuildLiveRenderable(), async updateTar
464468
Telemetry.RecordError(errorMessage, ex);
465469
InteractionService.DisplayError(errorMessage);
466470
// Don't display raw output - it's already in the log file
467-
InteractionService.DisplayMessage(KnownEmojis.PageFacingUp, string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, ExecutionContext.LogFilePath));
471+
InteractionService.DisplayMessage(
472+
KnownEmojis.PageFacingUp,
473+
string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, MarkupHelpers.SafeFileLink(InteractionService, ExecutionContext.LogFilePath)),
474+
allowMarkup: true);
468475
return ExitCodeConstants.FailedToDotnetRunAppHost;
469476
}
470477
catch (ConnectionLostException) when (isExtensionHost)
@@ -480,7 +487,10 @@ await InteractionService.DisplayLiveAsync(BuildLiveRenderable(), async updateTar
480487
Telemetry.RecordError(errorMessage, ex);
481488
InteractionService.DisplayError(errorMessage);
482489
// Don't display raw output - it's already in the log file
483-
InteractionService.DisplayMessage(KnownEmojis.PageFacingUp, string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, ExecutionContext.LogFilePath));
490+
InteractionService.DisplayMessage(
491+
KnownEmojis.PageFacingUp,
492+
string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, MarkupHelpers.SafeFileLink(InteractionService, ExecutionContext.LogFilePath)),
493+
allowMarkup: true);
484494
return ExitCodeConstants.FailedToDotnetRunAppHost;
485495
}
486496
finally
@@ -597,7 +607,7 @@ IRenderable LabelMarkup(string label)
597607
}
598608

599609
// Logs row
600-
grid.AddRow(LabelMarkup(logsLabel), new Text(logFilePath));
610+
grid.AddRow(LabelMarkup(logsLabel), new Markup(MarkupHelpers.SafeFileLink(console, logFilePath)));
601611

602612
// PID row (if provided)
603613
if (pid.HasValue)

src/Aspire.Cli/Commands/TelemetryCommandHelpers.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,10 @@ public static void DisplayTelemetryError(
324324
interactionService.DisplayMessage(KnownEmojis.Information, hint);
325325
}
326326

327-
interactionService.DisplayMessage(KnownEmojis.PageFacingUp, string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, logFilePath));
327+
interactionService.DisplayMessage(
328+
KnownEmojis.PageFacingUp,
329+
string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, MarkupHelpers.SafeFileLink(interactionService, logFilePath)),
330+
allowMarkup: true);
328331
}
329332

330333
/// <summary>

src/Aspire.Cli/Diagnostics/StartupErrorWriter.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ public void Dispose()
6464
}
6565

6666
var prefix = ConsoleHelpers.FormatEmojiPrefix(KnownEmojis.PageFacingUp, _errorConsole);
67-
_errorConsole.MarkupLine(prefix + string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, _logFilePath.EscapeMarkup()));
67+
var supportsLinks = _errorConsole.Profile.Capabilities.Links;
68+
var message = string.Format(
69+
CultureInfo.CurrentCulture,
70+
InteractionServiceStrings.SeeLogsAt,
71+
MarkupHelpers.SafeFileLink(supportsLinks, _logFilePath));
72+
_errorConsole.MarkupLine(prefix + message);
6873
}
6974
}

src/Aspire.Cli/Interaction/ConsoleInteractionService.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,10 @@ public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, stri
338338
return ExitCodeConstants.AppHostIncompatible;
339339
}
340340

341-
public void DisplayError(string errorMessage)
341+
public void DisplayError(string errorMessage, bool allowMarkup = false)
342342
{
343-
DisplayMessage(KnownEmojis.CrossMark, $"[red bold]{errorMessage.EscapeMarkup()}[/]", allowMarkup: true);
343+
var formatted = allowMarkup ? errorMessage : errorMessage.EscapeMarkup();
344+
DisplayMessage(KnownEmojis.CrossMark, $"[red bold]{formatted}[/]", allowMarkup: true);
344345
}
345346

346347
public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false)

src/Aspire.Cli/Interaction/ExtensionInteractionService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, stri
337337
return _consoleInteractionService.DisplayIncompatibleVersionError(ex, appHostHostingSdkVersion);
338338
}
339339

340-
public void DisplayError(string errorMessage)
340+
public void DisplayError(string errorMessage, bool allowMarkup = false)
341341
{
342342
// Serialize the local console write onto the same channel as the backchannel call so
343343
// it stays ordered with prior queued operations (e.g. DisplayLines). Otherwise the
@@ -347,7 +347,7 @@ public void DisplayError(string errorMessage)
347347
var result = _extensionTaskChannel.Writer.TryWrite(async () =>
348348
{
349349
await Backchannel.DisplayErrorAsync(errorMessage.RemoveSpectreFormatting(), _cancellationToken);
350-
_consoleInteractionService.DisplayError(errorMessage);
350+
_consoleInteractionService.DisplayError(errorMessage, allowMarkup);
351351
});
352352
Debug.Assert(result);
353353
}

0 commit comments

Comments
 (0)