feat: surface Copilot usage and quota data across the UI

Add three layers of usage visibility:

- ChatPane footer: premium request count, AIU consumed, and quota
  remaining below the existing context-window bar
- Settings / CopilotStatusCard: on-demand account quota section with
  progress bars, overage indicators, and reset dates fetched via
  the new get-quota sidecar command
- Activity Panel: per-agent token/cost/duration totals on each agent
  row and a Session Usage summary section between agents and timeline

Backend (sidecar):
- New get-quota command using SDK account.getQuota RPC
- New assistant-usage turn-scoped event from SDK assistant.usage
- QuotaSnapshotMapper for both typed and untyped SDK quota payloads
- DTOs: GetQuotaCommandDto, QuotaSnapshotDto, AccountQuotaResultEventDto,
  AssistantUsageEventDto

Frontend:
- Shared types: AssistantUsageEvent, QuotaSnapshot, GetQuotaCommand
- IPC bridge: getQuota channel, assistant-usage event dispatch
- State: SessionRequestUsageMap accumulator with per-agent breakdown
- 8 new tests for accumulator logic and formatting helpers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-29 00:46:09 +01:00
co-authored by Copilot
parent 48efbf36f9
commit 92832c6116
26 changed files with 978 additions and 18 deletions
@@ -18,6 +18,7 @@ public sealed class SidecarProtocolHost
private const string ListSessionsCommandType = "list-sessions";
private const string DeleteSessionCommandType = "delete-session";
private const string DisconnectSessionCommandType = "disconnect-session";
private const string GetQuotaCommandType = "get-quota";
private const string AskUserToolName = "ask_user";
private static readonly HashSet<string> ExcludedRuntimeToolNames = new(StringComparer.OrdinalIgnoreCase)
{
@@ -82,6 +83,7 @@ public sealed class SidecarProtocolHost
[ListSessionsCommandType] = HandleListSessionsAsync,
[DeleteSessionCommandType] = HandleDeleteSessionAsync,
[DisconnectSessionCommandType] = HandleDisconnectSessionAsync,
[GetQuotaCommandType] = HandleGetQuotaAsync,
};
}
@@ -313,6 +315,23 @@ public sealed class SidecarProtocolHost
}, context.CancellationToken).ConfigureAwait(false);
}
private async Task HandleGetQuotaAsync(CommandContext context)
{
_ = DeserializeCommand<GetQuotaCommandDto>(context);
IReadOnlyDictionary<string, QuotaSnapshotDto> quotaSnapshots =
await _sessionManager.GetQuotaAsync(context.CancellationToken).ConfigureAwait(false);
await WriteAsync(context.Output, new AccountQuotaResultEventDto
{
Type = "quota-result",
RequestId = context.Envelope.RequestId,
QuotaSnapshots = quotaSnapshots.ToDictionary(
snapshot => snapshot.Key,
snapshot => snapshot.Value,
StringComparer.Ordinal),
}, context.CancellationToken).ConfigureAwait(false);
}
private TCommand DeserializeCommand<TCommand>(CommandContext context)
where TCommand : SidecarCommandEnvelope
{