mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
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:
@@ -2,12 +2,18 @@ import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import {
|
||||
applySessionEventActivity,
|
||||
applyAssistantUsageEvent,
|
||||
buildAgentActivityRows,
|
||||
formatAgentActivityLabel,
|
||||
formatDuration,
|
||||
formatNanoAiu,
|
||||
formatTokenCount,
|
||||
isAgentActivityActive,
|
||||
isAgentActivityCompleted,
|
||||
pruneSessionActivities,
|
||||
pruneSessionRequestUsage,
|
||||
type SessionActivityMap,
|
||||
type SessionRequestUsageMap,
|
||||
} from '@renderer/lib/sessionActivity';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
@@ -379,3 +385,145 @@ describe('session activity helpers', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('assistant usage accumulator', () => {
|
||||
function makeUsageEvent(overrides: Partial<SessionEventRecord> = {}): SessionEventRecord {
|
||||
return {
|
||||
sessionId: 'session-1',
|
||||
kind: 'assistant-usage',
|
||||
occurredAt: '2026-03-23T00:00:00.000Z',
|
||||
agentId: 'architect',
|
||||
agentName: 'Architect',
|
||||
usageModel: 'gpt-5.4',
|
||||
usageInputTokens: 1200,
|
||||
usageOutputTokens: 300,
|
||||
usageCost: 0.42,
|
||||
usageDuration: 8200,
|
||||
usageTotalNanoAiu: 1_200_000_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('accumulates session-level totals from assistant-usage events', () => {
|
||||
let state: SessionRequestUsageMap = {};
|
||||
state = applyAssistantUsageEvent(state, makeUsageEvent());
|
||||
state = applyAssistantUsageEvent(state, makeUsageEvent({
|
||||
usageInputTokens: 800,
|
||||
usageOutputTokens: 200,
|
||||
usageCost: 0.28,
|
||||
usageDuration: 5000,
|
||||
usageTotalNanoAiu: 2_400_000_000,
|
||||
}));
|
||||
|
||||
const usage = state['session-1']!;
|
||||
expect(usage.requestCount).toBe(2);
|
||||
expect(usage.totalInputTokens).toBe(2000);
|
||||
expect(usage.totalOutputTokens).toBe(500);
|
||||
expect(usage.totalCost).toBeCloseTo(0.70);
|
||||
expect(usage.totalDurationMs).toBe(13200);
|
||||
expect(usage.totalNanoAiu).toBe(2_400_000_000);
|
||||
});
|
||||
|
||||
test('accumulates per-agent totals keyed by agentId', () => {
|
||||
let state: SessionRequestUsageMap = {};
|
||||
state = applyAssistantUsageEvent(state, makeUsageEvent({ agentId: 'architect' }));
|
||||
state = applyAssistantUsageEvent(state, makeUsageEvent({
|
||||
agentId: 'reviewer',
|
||||
agentName: 'Reviewer',
|
||||
usageInputTokens: 500,
|
||||
usageOutputTokens: 100,
|
||||
usageCost: 0.10,
|
||||
usageDuration: 3000,
|
||||
}));
|
||||
state = applyAssistantUsageEvent(state, makeUsageEvent({
|
||||
agentId: 'architect',
|
||||
usageInputTokens: 600,
|
||||
usageOutputTokens: 150,
|
||||
usageCost: 0.20,
|
||||
usageDuration: 4000,
|
||||
}));
|
||||
|
||||
const usage = state['session-1']!;
|
||||
expect(usage.perAgent['architect']!.requestCount).toBe(2);
|
||||
expect(usage.perAgent['architect']!.inputTokens).toBe(1800);
|
||||
expect(usage.perAgent['reviewer']!.requestCount).toBe(1);
|
||||
expect(usage.perAgent['reviewer']!.inputTokens).toBe(500);
|
||||
});
|
||||
|
||||
test('ignores non-assistant-usage events', () => {
|
||||
const state: SessionRequestUsageMap = {};
|
||||
const result = applyAssistantUsageEvent(state, {
|
||||
sessionId: 'session-1',
|
||||
kind: 'session-usage',
|
||||
occurredAt: '2026-03-23T00:00:00.000Z',
|
||||
tokenLimit: 100000,
|
||||
currentTokens: 5000,
|
||||
});
|
||||
expect(result).toBe(state);
|
||||
});
|
||||
|
||||
test('stores latest quota snapshots', () => {
|
||||
const snapshots = {
|
||||
premium_interactions: {
|
||||
entitlementRequests: 50,
|
||||
usedRequests: 12,
|
||||
remainingPercentage: 76,
|
||||
overage: 0,
|
||||
overageAllowedWithExhaustedQuota: true,
|
||||
resetDate: '2026-04-01T00:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
let state: SessionRequestUsageMap = {};
|
||||
state = applyAssistantUsageEvent(state, makeUsageEvent({ usageQuotaSnapshots: snapshots }));
|
||||
|
||||
expect(state['session-1']!.latestQuotaSnapshots).toEqual(snapshots);
|
||||
});
|
||||
|
||||
test('prunes request usage for removed sessions', () => {
|
||||
const current: SessionRequestUsageMap = {
|
||||
'session-1': {
|
||||
totalInputTokens: 1000,
|
||||
totalOutputTokens: 200,
|
||||
totalCost: 0.3,
|
||||
totalDurationMs: 5000,
|
||||
totalNanoAiu: 1_000_000_000,
|
||||
requestCount: 1,
|
||||
perAgent: {},
|
||||
},
|
||||
'session-2': {
|
||||
totalInputTokens: 500,
|
||||
totalOutputTokens: 100,
|
||||
totalCost: 0.1,
|
||||
totalDurationMs: 2000,
|
||||
totalNanoAiu: 500_000_000,
|
||||
requestCount: 1,
|
||||
perAgent: {},
|
||||
},
|
||||
};
|
||||
|
||||
const pruned = pruneSessionRequestUsage(current, ['session-2']);
|
||||
expect(pruned).toEqual({ 'session-2': current['session-2'] });
|
||||
expect(pruned).not.toBe(current);
|
||||
});
|
||||
});
|
||||
|
||||
describe('usage formatting helpers', () => {
|
||||
test('formatTokenCount formats values at different scales', () => {
|
||||
expect(formatTokenCount(500)).toBe('500');
|
||||
expect(formatTokenCount(1200)).toBe('1.2k');
|
||||
expect(formatTokenCount(45300)).toBe('45.3k');
|
||||
expect(formatTokenCount(1_500_000)).toBe('1.5M');
|
||||
});
|
||||
|
||||
test('formatNanoAiu converts nano-AIU to human-readable', () => {
|
||||
expect(formatNanoAiu(420_000_000)).toBe('0.42');
|
||||
expect(formatNanoAiu(12_300_000_000)).toBe('12.3');
|
||||
expect(formatNanoAiu(150_000_000_000)).toBe('150');
|
||||
});
|
||||
|
||||
test('formatDuration formats milliseconds', () => {
|
||||
expect(formatDuration(8200)).toBe('8.2s');
|
||||
expect(formatDuration(150_000)).toBe('2.5m');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user