From 04f2c0efae6f5a2dc79818fa4f05de5649559f10 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 15:21:27 +0000
Subject: [PATCH 1/4] Initial plan
From d29514234a87036f4fa93b6009403cfb4d9f5386 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 15:32:25 +0000
Subject: [PATCH 2/4] feat: split console logging package
Co-authored-by: kzu <169707+kzu@users.noreply.github.com>
---
Extensions.AI.slnx | 1 +
.../Console/ConsoleExtensions.cs | 0
.../Console/JsonConsoleLoggingExtensions.cs | 0
.../Console/JsonConsoleOptions.cs | 0
.../Console/YamlConsoleLoggingExtensions.cs | 99 ++++++++++
.../Console/YamlConsoleOptions.cs | 180 ++++++++++++++++++
.../Extensions.Console.csproj | 35 ++++
src/Extensions/Extensions.csproj | 5 +-
src/Tests/ConsoleLoggingTests.cs | 43 +++++
src/Tests/Tests.csproj | 1 +
10 files changed, 361 insertions(+), 3 deletions(-)
rename src/{Extensions => Extensions.Console}/Console/ConsoleExtensions.cs (100%)
rename src/{Extensions => Extensions.Console}/Console/JsonConsoleLoggingExtensions.cs (100%)
rename src/{Extensions => Extensions.Console}/Console/JsonConsoleOptions.cs (100%)
create mode 100644 src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs
create mode 100644 src/Extensions.Console/Console/YamlConsoleOptions.cs
create mode 100644 src/Extensions.Console/Extensions.Console.csproj
create mode 100644 src/Tests/ConsoleLoggingTests.cs
diff --git a/Extensions.AI.slnx b/Extensions.AI.slnx
index 9690911..a55a983 100644
--- a/Extensions.AI.slnx
+++ b/Extensions.AI.slnx
@@ -6,5 +6,6 @@
+
diff --git a/src/Extensions/Console/ConsoleExtensions.cs b/src/Extensions.Console/Console/ConsoleExtensions.cs
similarity index 100%
rename from src/Extensions/Console/ConsoleExtensions.cs
rename to src/Extensions.Console/Console/ConsoleExtensions.cs
diff --git a/src/Extensions/Console/JsonConsoleLoggingExtensions.cs b/src/Extensions.Console/Console/JsonConsoleLoggingExtensions.cs
similarity index 100%
rename from src/Extensions/Console/JsonConsoleLoggingExtensions.cs
rename to src/Extensions.Console/Console/JsonConsoleLoggingExtensions.cs
diff --git a/src/Extensions/Console/JsonConsoleOptions.cs b/src/Extensions.Console/Console/JsonConsoleOptions.cs
similarity index 100%
rename from src/Extensions/Console/JsonConsoleOptions.cs
rename to src/Extensions.Console/Console/JsonConsoleOptions.cs
diff --git a/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs b/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs
new file mode 100644
index 0000000..2db290d
--- /dev/null
+++ b/src/Extensions.Console/Console/YamlConsoleLoggingExtensions.cs
@@ -0,0 +1,99 @@
+using System.ClientModel.Primitives;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Devlooped.Extensions.AI;
+using Spectre.Console;
+
+namespace Microsoft.Extensions.AI;
+
+///
+/// Adds YAML console logging capabilities to the chat client and pipeline transport.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+public static class YamlConsoleLoggingExtensions
+{
+ extension(TOptions pipelineOptions) where TOptions : ClientPipelineOptions
+ {
+ ///
+ /// Observes the HTTP request and response messages from the underlying pipeline and renders them
+ /// to the console using Spectre.Console YAML formatting, but only if the console is interactive.
+ ///
+ /// The options type to configure for HTTP logging.
+ /// The options instance to configure.
+ ///
+ public TOptions UseYamlConsoleLogging(YamlConsoleOptions? consoleOptions = null)
+ {
+ consoleOptions ??= YamlConsoleOptions.Default;
+
+ if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable YAML console logging for HTTP pipeline messages?"))
+ return pipelineOptions;
+
+ if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive)
+ return pipelineOptions;
+
+ return pipelineOptions.Observe(
+ request => AnsiConsole.Write(consoleOptions.CreatePanel(request)),
+ response => AnsiConsole.Write(consoleOptions.CreatePanel(response)));
+ }
+ }
+
+ extension(ChatClientBuilder builder)
+ {
+ ///
+ /// Renders chat messages and responses to the console using Spectre.Console YAML formatting.
+ ///
+ /// The builder in use.
+ ///
+ /// Confirmation will be asked if the console is interactive, otherwise, it will be
+ /// enabled unconditionally.
+ ///
+ public ChatClientBuilder UseYamlConsoleLogging(YamlConsoleOptions? consoleOptions = null)
+ {
+ consoleOptions ??= YamlConsoleOptions.Default;
+
+ if (consoleOptions.InteractiveConfirm && ConsoleExtensions.IsConsoleInteractive && !AnsiConsole.Confirm("Do you want to enable YAML console logging for HTTP pipeline messages?"))
+ return builder;
+
+ if (consoleOptions.InteractiveOnly && !ConsoleExtensions.IsConsoleInteractive)
+ return builder;
+
+ return builder.Use(inner => new YamlConsoleLoggingChatClient(inner, consoleOptions));
+ }
+ }
+
+ class YamlConsoleLoggingChatClient(IChatClient inner, YamlConsoleOptions consoleOptions) : DelegatingChatClient(inner)
+ {
+ public override async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ AnsiConsole.Write(consoleOptions.CreatePanel(new
+ {
+ messages = messages.Where(x => x.Role != ChatRole.System).ToArray(),
+ options
+ }));
+
+ var response = await InnerClient.GetResponseAsync(messages, options, cancellationToken);
+ AnsiConsole.Write(consoleOptions.CreatePanel(response));
+
+ return response;
+ }
+
+ public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ AnsiConsole.Write(consoleOptions.CreatePanel(new
+ {
+ messages = messages.Where(x => x.Role != ChatRole.System).ToArray(),
+ options
+ }));
+
+ List updates = [];
+
+ await foreach (var update in base.GetStreamingResponseAsync(messages, options, cancellationToken))
+ {
+ updates.Add(update);
+ yield return update;
+ }
+
+ AnsiConsole.Write(consoleOptions.CreatePanel(updates));
+ }
+ }
+}
diff --git a/src/Extensions.Console/Console/YamlConsoleOptions.cs b/src/Extensions.Console/Console/YamlConsoleOptions.cs
new file mode 100644
index 0000000..800b9e4
--- /dev/null
+++ b/src/Extensions.Console/Console/YamlConsoleOptions.cs
@@ -0,0 +1,180 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.AI;
+using Spectre.Console;
+using Spectre.Console.Rendering;
+using YamlDotNet.Serialization;
+
+namespace Devlooped.Extensions.AI;
+
+///
+/// Options for rendering YAML output to the console.
+///
+public class YamlConsoleOptions
+{
+ static readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web)
+ {
+ DefaultIgnoreCondition =
+ JsonIgnoreCondition.WhenWritingNull |
+ JsonIgnoreCondition.WhenWritingDefault
+ };
+
+ static readonly ISerializer serializer = new SerializerBuilder().Build();
+
+ ///
+ /// Default settings for rendering YAML output to the console, which include:
+ /// * : true
+ /// * : true if console is interactive, otherwise false
+ /// * : true
+ ///
+ public static YamlConsoleOptions Default { get; } = new YamlConsoleOptions();
+
+ ///
+ /// Border kind for the YAML output panel.
+ ///
+ public BoxBorder Border { get; set; } = BoxBorder.Square;
+
+ ///
+ /// Border style for the YAML output panel.
+ ///
+ public Style BorderStyle { get; set; } = Style.Parse("grey");
+
+ ///
+ /// Whether to include additional properties in the YAML output.
+ ///
+ ///
+ /// See and .
+ ///
+ public bool IncludeAdditionalProperties { get; set; } = true;
+
+ ///
+ /// Confirm whether to render YAML output to the console, if the console is interactive. If
+ /// it is non-interactive, YAML output will be rendered conditionally based on the
+ /// setting.
+ ///
+ public bool InteractiveConfirm { get; set; } = ConsoleExtensions.IsConsoleInteractive;
+
+ ///
+ /// Only render YAML output if the console is interactive.
+ ///
+ ///
+ /// This setting defaults to to avoid cluttering non-interactive console
+ /// outputs with YAML, while also removing the need to conditionally check for console interactivity.
+ ///
+ public bool InteractiveOnly { get; set; } = true;
+
+ ///
+ /// Specifies the length at which long text will be truncated.
+ ///
+ public int? TruncateLength { get; set; }
+
+ ///
+ /// Specifies the length at which long text will be wrapped automatically.
+ ///
+ public int? WrapLength { get; set; }
+
+ internal Panel CreatePanel(object value)
+ {
+ var yaml = ToYamlString(value);
+
+ return new Panel(WrapLength.HasValue ? new WrappedText(yaml, WrapLength.Value) : new Text(yaml, Style.Plain))
+ {
+ Border = Border,
+ BorderStyle = BorderStyle,
+ };
+ }
+
+ internal string ToYamlString(object? value)
+ {
+ if (value is null)
+ return string.Empty;
+
+ JsonNode? node = value switch
+ {
+ JsonNode existing => existing,
+ _ => JsonSerializer.SerializeToNode(value, value.GetType(), jsonOptions),
+ };
+
+ if (node is null)
+ return string.Empty;
+
+ if (TruncateLength.HasValue || !IncludeAdditionalProperties)
+ node = JsonNode.Parse(node.ToShortJsonString(TruncateLength, IncludeAdditionalProperties));
+
+ var yaml = serializer.Serialize(ToPlain(node));
+ return yaml.TrimEnd();
+ }
+
+ static object? ToPlain(JsonNode? node)
+ {
+ return node switch
+ {
+ null => null,
+ JsonValue value => GetValue(value),
+ JsonObject obj => obj.ToDictionary(kv => kv.Key, kv => ToPlain(kv.Value)),
+ JsonArray arr => arr.Select(ToPlain).ToArray(),
+ _ => node.ToJsonString()
+ };
+ }
+
+ static object? GetValue(JsonValue value)
+ {
+ if (value.TryGetValue(out bool boolean))
+ return boolean;
+ if (value.TryGetValue(out int number))
+ return number;
+ if (value.TryGetValue(out long longNumber))
+ return longNumber;
+ if (value.TryGetValue(out double real))
+ return real;
+ if (value.TryGetValue(out string? str))
+ return str;
+
+ return value.GetValue