Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Allow multiple clarifications in one response
  • Loading branch information
westey-m committed Apr 29, 2026
commit 7d7a5ec1fa9d673c1d4749c827bdbe1ecc3d2ee7
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,7 @@ private async Task WriteInfoCoreAsync(string text, ConsoleColor? color, bool new
string prefix = this._lastWasText ? "\n\n " : " ";
this._lastWasText = false;

if (color.HasValue)
{
System.Console.ForegroundColor = color.Value;
}
System.Console.ForegroundColor = color ?? GetModeColor(this.CurrentMode, this._modeColors);

if (newLine)
{
Expand All @@ -93,10 +90,7 @@ private async Task WriteInfoCoreAsync(string text, ConsoleColor? color, bool new
System.Console.Write(prefix + text);
}

if (color.HasValue)
{
System.Console.ForegroundColor = GetModeColor(this.CurrentMode, this._modeColors);
}
System.Console.ForegroundColor = GetModeColor(this.CurrentMode, this._modeColors);

this._spinner.Start();
}
Expand Down Expand Up @@ -186,7 +180,6 @@ public async Task<string> ReadSelectionAsync(string title, IList<string> choices
{
await this._spinner.StopAsync();

System.Console.WriteLine();
AnsiConsole.Write(this.CreateModeRule());

const string FreeformOption = "✏️ Type a custom response...";
Expand Down Expand Up @@ -227,7 +220,6 @@ public async Task WriteStreamFooterAsync(bool hasApprovalRequests)

System.Console.ResetColor();
System.Console.WriteLine();
System.Console.WriteLine();
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.

using System.Text;
using System.Text.Json;
Expand Down Expand Up @@ -78,12 +78,19 @@ public override Task OnTextAsync(ConsoleWriter writer, string text)
// Render based on response type.
if (planningResponse.Type == PlanningResponseType.Clarification)
{
return AsUserMessages(await this.RenderClarificationAndCollectResponseAsync(writer, planningResponse));
return AsUserMessages(await this.RenderClarificationsAndCollectResponsesAsync(writer, planningResponse));
}

if (planningResponse.Type == PlanningResponseType.Approval)
{
string response = await this.RenderApprovalAndCollectResponseAsync(writer, planningResponse, options);
var question = planningResponse.Questions.FirstOrDefault();
if (question is null)
{
await writer.WriteInfoLineAsync("(approval response had no content)", ConsoleColor.DarkYellow);
return null;
}

string response = await this.RenderApprovalAndCollectResponseAsync(writer, question, options);
if (response == "Approved")
{
this._modeProvider.SetMode(session, options.ExecutionModeName!);
Expand All @@ -102,24 +109,39 @@ await writer.WriteInfoLineAsync($"✅ Switched to {options.ExecutionModeName} mo
private static IList<ChatMessage>? AsUserMessages(string? text) =>
text is not null ? [new ChatMessage(ChatRole.User, text)] : null;

private async Task<string?> RenderClarificationAndCollectResponseAsync(ConsoleWriter writer, PlanningResponse response)
private async Task<string?> RenderClarificationsAndCollectResponsesAsync(ConsoleWriter writer, PlanningResponse response)
{
await writer.WriteInfoLineAsync(response.Message ?? string.Empty);
var answers = new List<string>();

if (response.Choices is { Count: > 0 })
foreach (var question in response.Questions)
{
return await writer.ReadSelectionAsync(
"Choose an option:",
response.Choices);
await writer.WriteInfoLineAsync(string.Empty);
await writer.WriteInfoLineAsync(question.Message);

string? answer;
if (question.Choices is { Count: > 0 })
{
answer = await writer.ReadSelectionAsync(
"Choose an option:",
question.Choices);
}
else
{
answer = (await writer.ReadLineAsync("Response: "))?.Trim();
}

if (!string.IsNullOrWhiteSpace(answer))
{
answers.Add($"Q: {question.Message}\nA: {answer}");
}
}

string? input = (await writer.ReadLineAsync("Response: "))?.Trim();
return string.IsNullOrWhiteSpace(input) ? null : input;
return answers.Count > 0 ? string.Join("\n\n", answers) : null;
}

private async Task<string> RenderApprovalAndCollectResponseAsync(ConsoleWriter writer, PlanningResponse response, HarnessConsoleOptions options)
private async Task<string> RenderApprovalAndCollectResponseAsync(ConsoleWriter writer, PlanningQuestion question, HarnessConsoleOptions options)
{
await writer.WriteInfoLineAsync(response.Message ?? string.Empty);
await writer.WriteInfoLineAsync(question.Message);

var choices = new List<string>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.

using System.ComponentModel;
using System.Text.Json.Serialization;
Expand All @@ -18,17 +18,32 @@ public class PlanningResponse
[JsonPropertyName("type")]
public required PlanningResponseType Type { get; set; }

/// <summary>
/// Gets or sets the list of questions or items to present to the user.
/// For clarification, this contains one or more questions (each with choices).
/// For approval, this contains exactly one item with the plan summary.
/// </summary>
[JsonPropertyName("questions")]
[Description("For clarifications, this has one or more questions to ask the user (each with choices). For approvals, this has exactly one item containing the plan summary for the user to approve.")]
public required List<PlanningQuestion> Questions { get; set; }
}

/// <summary>
/// Represents a single question or item within a <see cref="PlanningResponse"/>.
/// </summary>
public class PlanningQuestion
{
/// <summary>
/// Gets or sets the message to display to the user.
/// For clarification, this is the question. For approval, this is a summary of what the agent plans to do.
/// For clarification, this is the question. For approval, this is the plan summary.
/// </summary>
[JsonPropertyName("message")]
[Description("For clarifications, this has the question that needs to be clarified with the user. For approvals, this would contain a summary of the execution plan that the user needs to approve.")]
public required string Message { get; set; }

/// <summary>
/// Gets or sets the list of choices for the user to pick from.
/// Only used when <see cref="Type"/> is <see cref="PlanningResponseType.Clarification"/>.
/// Only used for clarification questions. Null when no predefined choices are offered.
/// </summary>
[JsonPropertyName("choices")]
[Description("For clarifications, this has a list of options that the user can choose from. null for approvals.")]
Expand Down
Loading