fix: suppress noisy hook lifecycle events

Stop forwarding hook lifecycle events when a project has no configured
.github/hooks/*.json commands so the UI only receives meaningful hook
activity. Also expose the configured-hook state on the bundle, cover the
suppression wiring with tests, and document the updated event semantics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-28 14:05:10 +01:00
co-authored by Copilot
parent db9ffe8399
commit 1fbfdbbac4
7 changed files with 118 additions and 22 deletions
@@ -13,13 +13,16 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
{
private readonly List<IAsyncDisposable> _disposables = [];
private CopilotAgentBundle(IReadOnlyList<AIAgent> agents)
internal CopilotAgentBundle(IReadOnlyList<AIAgent> agents, bool hasConfiguredHooks)
{
Agents = agents;
HasConfiguredHooks = hasConfiguredHooks;
}
public IReadOnlyList<AIAgent> Agents { get; }
public bool HasConfiguredHooks { get; }
public static async Task<CopilotAgentBundle> CreateAsync(
RunTurnCommandDto command,
Func<PatternAgentDefinitionDto, PermissionRequest, PermissionInvocation, Task<PermissionRequestResult>> onPermissionRequest,
@@ -72,7 +75,7 @@ internal sealed class CopilotAgentBundle : IAsyncDisposable
disposables.Add(agent);
}
CopilotAgentBundle bundle = new(agents);
CopilotAgentBundle bundle = new(agents, hasConfiguredHooks: !configuredHooks.IsEmpty);
bundle._disposables.AddRange(disposables);
return bundle;
}
@@ -28,6 +28,8 @@ internal sealed class CopilotTurnExecutionState
public bool HasPendingExitPlanModeRequest { get; private set; }
public bool SuppressHookLifecycleEvents { get; set; }
public async Task EmitThinkingIfNeeded(
AgentIdentity agent,
Func<SidecarEventDto, Task> onEvent)
@@ -113,11 +115,17 @@ internal sealed class CopilotTurnExecutionState
break;
case HookStartEvent hookStart:
ActiveAgent = agent;
_pendingEvents.Enqueue(CreateHookLifecycleEvent(agent, "start", hookStart.Data));
if (!SuppressHookLifecycleEvents)
{
_pendingEvents.Enqueue(CreateHookLifecycleEvent(agent, "start", hookStart.Data));
}
break;
case HookEndEvent hookEnd:
ActiveAgent = agent;
_pendingEvents.Enqueue(CreateHookLifecycleEvent(agent, "end", hookEnd.Data));
if (!SuppressHookLifecycleEvents)
{
_pendingEvents.Enqueue(CreateHookLifecycleEvent(agent, "end", hookEnd.Data));
}
break;
case SessionUsageInfoEvent usageInfo:
ActiveAgent = agent;
@@ -75,6 +75,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
}
},
runCancellation.Token);
ConfigureHookLifecycleEventSuppression(state, bundle);
Workflow workflow = bundle.BuildWorkflow(command.Pattern);
List<ChatMessage> inputMessages = command.Messages.Select(WorkflowTranscriptProjector.ToChatMessage).ToList();
WorkflowTranscriptProjector.AttachMessageMode(inputMessages, command.MessageMode);
@@ -118,6 +119,16 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
}
}
internal static void ConfigureHookLifecycleEventSuppression(
CopilotTurnExecutionState state,
CopilotAgentBundle bundle)
{
ArgumentNullException.ThrowIfNull(state);
ArgumentNullException.ThrowIfNull(bundle);
state.SuppressHookLifecycleEvents = !bundle.HasConfiguredHooks;
}
private static async Task EmitPendingEventsAsync(
CopilotTurnExecutionState state,
Func<SidecarEventDto, Task> onEvent)