feat: emit tool-call file previews

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-29 23:55:25 +02:00
co-authored by Copilot
parent af69d494a5
commit e956f8ea6c
11 changed files with 346 additions and 6 deletions
@@ -19,6 +19,7 @@ internal sealed class CopilotApprovalCoordinator
private const string MemoryPermissionKind = "memory";
private const string CustomToolPermissionKind = "custom-tool";
private const string HookPermissionKind = "hook";
private const string ToolCallingActivityType = "tool-calling";
private readonly ConcurrentDictionary<string, PendingApprovalRequest> _pendingApprovals = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, byte>> _requestApprovedTools = new(StringComparer.Ordinal);
@@ -54,11 +55,40 @@ internal sealed class CopilotApprovalCoordinator
IReadOnlyDictionary<string, string> toolNamesByCallId,
Func<ApprovalRequestedEventDto, Task> onApproval,
CancellationToken cancellationToken)
{
return await RequestApprovalAsync(
command,
agent,
request,
invocation,
toolNamesByCallId,
onActivity: null,
onApproval,
cancellationToken)
.ConfigureAwait(false);
}
public async Task<PermissionRequestResult> RequestApprovalAsync(
RunTurnCommandDto command,
PatternAgentDefinitionDto agent,
PermissionRequest request,
PermissionInvocation invocation,
IReadOnlyDictionary<string, string> toolNamesByCallId,
Func<AgentActivityEventDto, Task>? onActivity,
Func<ApprovalRequestedEventDto, Task> onApproval,
CancellationToken cancellationToken)
{
string? toolName = ResolveApprovalToolName(request, toolNamesByCallId);
string? autoApprovedToolName = ResolveAutoApprovedToolName(request);
string? mcpServerApprovalKey = ResolveMcpServerApprovalKey(request);
string? approvalCacheKey = ResolveApprovalCacheKey(toolName, autoApprovedToolName);
AgentActivityEventDto? fileChangeActivity = BuildToolCallFileChangeActivity(command, agent, request, toolName);
if (fileChangeActivity is not null && onActivity is not null)
{
await onActivity(fileChangeActivity).ConfigureAwait(false);
}
if (IsToolApprovedForRequest(command.RequestId, approvalCacheKey)
|| !RequiresToolCallApproval(command.Pattern.ApprovalPolicy, agent.Id, toolName, autoApprovedToolName, mcpServerApprovalKey))
{
@@ -154,6 +184,46 @@ internal sealed class CopilotApprovalCoordinator
};
}
internal static AgentActivityEventDto? BuildToolCallFileChangeActivity(
RunTurnCommandDto command,
PatternAgentDefinitionDto agent,
PermissionRequest request,
string? toolName)
{
if (request is not PermissionRequestWrite write)
{
return null;
}
string? filePath = NormalizeOptionalString(write.FileName);
if (filePath is null)
{
return null;
}
string agentName = string.IsNullOrWhiteSpace(agent.Name) ? agent.Id : agent.Name;
return new AgentActivityEventDto
{
Type = "agent-activity",
RequestId = command.RequestId,
SessionId = command.SessionId,
ActivityType = ToolCallingActivityType,
AgentId = NormalizeOptionalString(agent.Id),
AgentName = NormalizeOptionalString(agentName),
ToolName = NormalizeOptionalString(toolName),
ToolCallId = NormalizeOptionalString(write.ToolCallId),
FileChanges =
[
new ToolCallFileChangeDto
{
Path = filePath,
Diff = NormalizeOptionalPreviewText(write.Diff),
NewFileContents = NormalizeOptionalPreviewText(write.NewFileContents),
},
],
};
}
internal static PermissionDetailDto BuildPermissionDetail(PermissionRequest request)
{
ArgumentNullException.ThrowIfNull(request);
@@ -495,6 +565,11 @@ internal sealed class CopilotApprovalCoordinator
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
private static string? NormalizeOptionalPreviewText(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value;
}
private static IReadOnlyList<string>? NormalizeOptionalStringList(IEnumerable<string?> values)
{
List<string> normalized = values
@@ -50,6 +50,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
request,
invocation,
state.ToolNamesByCallId,
activity => EmitActivityAsync(command, state, activity, onEvent),
onApproval,
runCancellation.Token),
(agent, request, invocation) => _userInputCoordinator.RequestUserInputAsync(
@@ -74,6 +74,7 @@ internal static class WorkflowRequestInfoInterpreter
AgentId = activeAgent.AgentId,
AgentName = activeAgent.AgentName,
ToolName = tool.ToolName,
ToolCallId = tool.ToolCallId,
};
}