Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
36 changes: 36 additions & 0 deletions apps/ccusage/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@
"description": "Force compact mode for narrow displays (better for screenshots)",
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
},
"breakdownSubagents": {
"type": "boolean",
"description": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"markdownDescription": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"default": false
}
},
"additionalProperties": false,
Expand Down Expand Up @@ -209,6 +215,12 @@
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
},
"breakdownSubagents": {
"type": "boolean",
"description": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"markdownDescription": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"default": false
},
"instances": {
"type": "boolean",
"description": "Show usage breakdown by project/instance",
Expand Down Expand Up @@ -323,6 +335,12 @@
"description": "Force compact mode for narrow displays (better for screenshots)",
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
},
"breakdownSubagents": {
"type": "boolean",
"description": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"markdownDescription": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"default": false
}
},
"additionalProperties": false
Expand Down Expand Up @@ -423,6 +441,12 @@
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
},
"breakdownSubagents": {
"type": "boolean",
"description": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"markdownDescription": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"default": false
},
"startOfWeek": {
"type": "string",
"enum": [
Expand Down Expand Up @@ -527,6 +551,12 @@
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
},
"breakdownSubagents": {
"type": "boolean",
"description": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"markdownDescription": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"default": false
},
"id": {
"type": "string",
"description": "Load usage data for a specific session ID",
Expand Down Expand Up @@ -631,6 +661,12 @@
"markdownDescription": "Force compact mode for narrow displays (better for screenshots)",
"default": false
},
"breakdownSubagents": {
"type": "boolean",
"description": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"markdownDescription": "Show subagent usage as separate breakdown rows (default: aggregated with main usage)",
"default": false
},
"active": {
"type": "boolean",
"description": "Show only active block with projections",
Expand Down
1 change: 1 addition & 0 deletions apps/ccusage/src/_daily-grouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function groupByProject(dailyData: DailyData): Record<string, DailyProjec
totalCost: data.totalCost,
modelsUsed: data.modelsUsed,
modelBreakdowns: data.modelBreakdowns,
...(data.subagentUsage != null && { subagentUsage: data.subagentUsage }),
});
}

Expand Down
3 changes: 2 additions & 1 deletion apps/ccusage/src/_json-output-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

import type { DailyDate, ModelName } from './_types.ts';
import type { ModelBreakdown } from './data-loader.ts';
import type { ModelBreakdown, SubagentUsageSummary } from './data-loader.ts';

/**
* Interface for daily command JSON output structure (groupByProject)
Expand All @@ -25,4 +25,5 @@ export type DailyProjectOutput = {
totalCost: number;
modelsUsed: ModelName[];
modelBreakdowns: ModelBreakdown[];
subagentUsage?: SubagentUsageSummary;
};
29 changes: 29 additions & 0 deletions apps/ccusage/src/_live-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,30 @@ export function renderLiveDisplay(terminal: TerminalManager, block: SessionBlock
}

terminal.write(`${marginStr}│${' '.repeat(boxWidth - 2)}│\n`);

// Subagent section (if present)
if (block.subagentUsage != null) {
terminal.write(`${marginStr}├${'─'.repeat(boxWidth - 2)}┤\n`);
terminal.write(`${marginStr}│${' '.repeat(boxWidth - 2)}│\n`);

const subagentLabel = pc.bold('SUBAGENTS');
const subagentInfo = `${block.subagentUsage.taskCount} tasks • ${formatTokensShort(block.subagentUsage.totalTokens)} tokens • ${formatCurrency(block.subagentUsage.totalCost)}`;
const subagentLine = `${subagentLabel} ${pc.cyan(subagentInfo)}`;
const subagentLinePadded = subagentLine + ' '.repeat(Math.max(0, boxWidth - 3 - stringWidth(subagentLine)));
terminal.write(`${marginStr}│ ${subagentLinePadded}│\n`);

// Subagent details (indented)
const inputStr = `${pc.gray('Input:')} ${formatTokensShort(block.subagentUsage.inputTokens)}`;
const outputStr = `${pc.gray('Output:')} ${formatTokensShort(block.subagentUsage.outputTokens)}`;
const cacheTotal = block.subagentUsage.cacheCreationTokens + block.subagentUsage.cacheReadTokens;
const cacheStr = `${pc.gray('Cache:')} ${formatTokensShort(cacheTotal)}`;
const subagentDetails = `${' '.repeat(detailsIndent)}${inputStr} ${outputStr} ${cacheStr}`;
const subagentDetailsPadded = subagentDetails + ' '.repeat(Math.max(0, boxWidth - 3 - stringWidth(subagentDetails)));
terminal.write(`${marginStr}│ ${subagentDetailsPadded}│\n`);

terminal.write(`${marginStr}│${' '.repeat(boxWidth - 2)}│\n`);
}

terminal.write(`${marginStr}├${'─'.repeat(boxWidth - 2)}┤\n`);
terminal.write(`${marginStr}│${' '.repeat(boxWidth - 2)}│\n`);

Expand Down Expand Up @@ -483,6 +507,11 @@ export function renderCompactLiveDisplay(
// Cost
terminal.write(`Cost: ${formatCurrency(block.costUSD)}\n`);

// Subagent info (if present)
if (block.subagentUsage != null) {
terminal.write(pc.cyan(`Subagents: ${block.subagentUsage.taskCount} tasks, ${formatTokensShort(block.subagentUsage.totalTokens)}, ${formatCurrency(block.subagentUsage.totalCost)}\n`));
}

// Burn rate
const burnRate = calculateBurnRate(block);
if (burnRate != null) {
Expand Down
129 changes: 129 additions & 0 deletions apps/ccusage/src/_session-blocks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ModelName } from './_types.ts';
import type { SubagentUsageSummary } from './data-loader.ts';
import { uniq } from 'es-toolkit';
import { DEFAULT_RECENT_DAYS } from './_consts.ts';
import { getTotalTokens } from './_token-utils.ts';
Expand Down Expand Up @@ -33,6 +35,7 @@ export type LoadedUsageEntry = {
model: string;
version?: string;
usageLimitResetTime?: Date; // Claude API usage limit reset time
isSidechain?: boolean; // Flag indicating this is a subagent/Task tool entry
};

/**
Expand Down Expand Up @@ -60,6 +63,7 @@ export type SessionBlock = {
costUSD: number;
models: string[];
usageLimitResetTime?: Date; // Claude API usage limit reset time
subagentUsage?: SubagentUsageSummary; // Subagent usage summary
};

/**
Expand Down Expand Up @@ -179,16 +183,55 @@ function createBlock(startTime: Date, entries: LoadedUsageEntry[], now: Date, se
const models: string[] = [];
let usageLimitResetTime: Date | undefined;

// Track subagent entries in single loop
const subagentTokenCounts: TokenCounts = {
inputTokens: 0,
outputTokens: 0,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
};
let subagentCost = 0;
let subagentCount = 0;
const subagentModels: string[] = [];

// Single loop to aggregate both total and subagent counts
for (const entry of entries) {
// Aggregate all entries
tokenCounts.inputTokens += entry.usage.inputTokens;
tokenCounts.outputTokens += entry.usage.outputTokens;
tokenCounts.cacheCreationInputTokens += entry.usage.cacheCreationInputTokens;
tokenCounts.cacheReadInputTokens += entry.usage.cacheReadInputTokens;
costUSD += entry.costUSD ?? 0;
usageLimitResetTime = entry.usageLimitResetTime ?? usageLimitResetTime;
models.push(entry.model);

// Aggregate subagent entries
if (entry.isSidechain === true) {
subagentTokenCounts.inputTokens += entry.usage.inputTokens;
subagentTokenCounts.outputTokens += entry.usage.outputTokens;
subagentTokenCounts.cacheCreationInputTokens += entry.usage.cacheCreationInputTokens;
subagentTokenCounts.cacheReadInputTokens += entry.usage.cacheReadInputTokens;
subagentCost += entry.costUSD ?? 0;
subagentCount++;
subagentModels.push(entry.model);
}
}

// Create subagent usage summary if there are subagent entries
const subagentUsage = subagentCount > 0
? {
totalTokens: subagentTokenCounts.inputTokens + subagentTokenCounts.outputTokens
+ subagentTokenCounts.cacheCreationInputTokens + subagentTokenCounts.cacheReadInputTokens,
inputTokens: subagentTokenCounts.inputTokens,
outputTokens: subagentTokenCounts.outputTokens,
cacheCreationTokens: subagentTokenCounts.cacheCreationInputTokens,
cacheReadTokens: subagentTokenCounts.cacheReadInputTokens,
totalCost: subagentCost,
taskCount: subagentCount,
modelsUsed: uniq(subagentModels) as ModelName[],
}
: undefined;

return {
id: startTime.toISOString(),
startTime,
Expand All @@ -200,6 +243,7 @@ function createBlock(startTime: Date, entries: LoadedUsageEntry[], now: Date, se
costUSD,
models: uniq(models),
usageLimitResetTime,
...(subagentUsage != null && { subagentUsage }),
};
}

Expand Down Expand Up @@ -1002,5 +1046,90 @@ if (import.meta.vitest != null) {
expect(blocksDefault[0]!.endTime).toEqual(blocksExplicit[0]!.endTime);
expect(blocksDefault[0]!.endTime).toEqual(new Date(baseTime.getTime() + 5 * 60 * 60 * 1000));
});

it('calculates subagent usage when isSidechain entries are present', () => {
const baseTime = new Date('2024-01-01T10:00:00Z');
const entries: LoadedUsageEntry[] = [
createMockEntry(baseTime),
{
...createMockEntry(new Date(baseTime.getTime() + 60 * 1000)),
isSidechain: true,
usage: {
inputTokens: 500,
outputTokens: 250,
cacheCreationInputTokens: 50,
cacheReadInputTokens: 100,
},
costUSD: 0.3,
},
{
...createMockEntry(new Date(baseTime.getTime() + 120 * 1000)),
isSidechain: true,
usage: {
inputTokens: 300,
outputTokens: 150,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 50,
},
costUSD: 0.2,
},
];

const blocks = identifySessionBlocks(entries);

expect(blocks).toHaveLength(1);
const block = blocks[0];
expect(block?.subagentUsage).toBeDefined();
expect(block?.subagentUsage?.taskCount).toBe(2);
expect(block?.subagentUsage?.inputTokens).toBe(800); // 500 + 300
expect(block?.subagentUsage?.outputTokens).toBe(400); // 250 + 150
expect(block?.subagentUsage?.cacheCreationTokens).toBe(50);
expect(block?.subagentUsage?.cacheReadTokens).toBe(150); // 100 + 50
expect(block?.subagentUsage?.totalTokens).toBe(1400); // 800 + 400 + 50 + 150
expect(block?.subagentUsage?.totalCost).toBe(0.5); // 0.3 + 0.2
});

it('does not include subagentUsage when no subagent entries', () => {
const baseTime = new Date('2024-01-01T10:00:00Z');
const entries: LoadedUsageEntry[] = [
createMockEntry(baseTime),
createMockEntry(new Date(baseTime.getTime() + 60 * 1000)),
];

const blocks = identifySessionBlocks(entries);

expect(blocks).toHaveLength(1);
const block = blocks[0];
expect(block?.subagentUsage).toBeUndefined();
});

it('aggregates both main and subagent tokens in total counts', () => {
const baseTime = new Date('2024-01-01T10:00:00Z');
const mainEntry = createMockEntry(baseTime);
const subagentEntry = {
...createMockEntry(new Date(baseTime.getTime() + 60 * 1000)),
isSidechain: true,
usage: {
inputTokens: 500,
outputTokens: 250,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
},
costUSD: 0.3,
};

const blocks = identifySessionBlocks([mainEntry, subagentEntry]);

expect(blocks).toHaveLength(1);
const block = blocks[0];
// Total should include both main and subagent
expect(block?.tokenCounts.inputTokens).toBe(mainEntry.usage.inputTokens + 500);
expect(block?.tokenCounts.outputTokens).toBe(mainEntry.usage.outputTokens + 250);
expect(block?.costUSD).toBe((mainEntry.costUSD ?? 0) + 0.3);
// Subagent usage should only include subagent entry
expect(block?.subagentUsage?.inputTokens).toBe(500);
expect(block?.subagentUsage?.outputTokens).toBe(250);
expect(block?.subagentUsage?.totalCost).toBe(0.3);
});
});
}
6 changes: 6 additions & 0 deletions apps/ccusage/src/_shared-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ export const sharedArgs = {
description: 'Force compact mode for narrow displays (better for screenshots)',
default: false,
},
breakdownSubagents: {
type: 'boolean',
short: 'a',
description: 'Show subagent usage as separate breakdown rows (default: aggregated with main usage)',
default: false,
},
} as const satisfies Args;

/**
Expand Down
Loading