Skip to content

Commit a5c4cc7

Browse files
authored
feat(statusline): add 'both' cost source option to display cc and ccusage costs (#574)
1 parent 837f60e commit a5c4cc7

File tree

6 files changed

+100
-38
lines changed

6 files changed

+100
-38
lines changed

config-schema.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -726,10 +726,11 @@
726726
"enum": [
727727
"auto",
728728
"ccusage",
729-
"cc"
729+
"cc",
730+
"both"
730731
],
731-
"description": "Session cost source: auto (prefer CC then ccusage), ccusage (always calculate), cc (always use Claude Code cost)",
732-
"markdownDescription": "Session cost source: auto (prefer CC then ccusage), ccusage (always calculate), cc (always use Claude Code cost)",
732+
"description": "Session cost source: auto (prefer CC then ccusage), ccusage (always calculate), cc (always use Claude Code cost), both (show both costs)",
733+
"markdownDescription": "Session cost source: auto (prefer CC then ccusage), ccusage (always calculate), cc (always use Claude Code cost), both (show both costs)",
733734
"default": "auto"
734735
},
735736
"cache": {

src/commands/statusline.ts

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ type SemaphoreType = {
7070
};
7171

7272
const visualBurnRateChoices = ['off', 'emoji', 'text', 'emoji-text'] as const;
73-
const costSourceChoices = ['auto', 'ccusage', 'cc'] as const;
73+
const costSourceChoices = ['auto', 'ccusage', 'cc', 'both'] as const;
7474

7575
export const statuslineCommand = define({
7676
name: 'statusline',
@@ -93,7 +93,7 @@ export const statuslineCommand = define({
9393
costSource: {
9494
type: 'enum',
9595
choices: costSourceChoices,
96-
description: 'Session cost source: auto (prefer CC then ccusage), ccusage (always calculate), cc (always use Claude Code cost)',
96+
description: 'Session cost source: auto (prefer CC then ccusage), ccusage (always calculate), cc (always use Claude Code cost), both (show both costs)',
9797
default: 'auto',
9898
short: 'cs',
9999
negatable: false,
@@ -227,16 +227,11 @@ export const statuslineCommand = define({
227227
await Result.try({
228228
try: async () => {
229229
// Determine session cost based on cost source
230-
const sessionCost = await (async (): Promise<number | undefined> => {
230+
const { sessionCost, ccCost, ccusageCost } = await (async (): Promise<{ sessionCost?: number; ccCost?: number; ccusageCost?: number }> => {
231231
const costSource = ctx.values.costSource;
232232

233-
// If 'cc' mode and cost is available from Claude Code, use it
234-
if (costSource === 'cc') {
235-
return hookData.cost?.total_cost_usd;
236-
}
237-
238-
// If 'ccusage' mode, always calculate using ccusage
239-
if (costSource === 'ccusage') {
233+
// Helper function to get ccusage cost
234+
const getCcusageCost = async (): Promise<number | undefined> => {
240235
return Result.pipe(
241236
Result.try({
242237
try: async () => loadSessionUsageById(sessionId, {
@@ -249,28 +244,37 @@ export const statuslineCommand = define({
249244
Result.inspectError(error => logger.error('Failed to load session data:', error)),
250245
Result.unwrap(undefined),
251246
);
247+
};
248+
249+
// If 'both' mode, calculate both costs
250+
if (costSource === 'both') {
251+
const ccCost = hookData.cost?.total_cost_usd;
252+
const ccusageCost = await getCcusageCost();
253+
return { ccCost, ccusageCost };
254+
}
255+
256+
// If 'cc' mode and cost is available from Claude Code, use it
257+
if (costSource === 'cc') {
258+
return { sessionCost: hookData.cost?.total_cost_usd };
259+
}
260+
261+
// If 'ccusage' mode, always calculate using ccusage
262+
if (costSource === 'ccusage') {
263+
const cost = await getCcusageCost();
264+
return { sessionCost: cost };
252265
}
253266

254267
// If 'auto' mode (default), prefer Claude Code cost, fallback to ccusage
255268
if (costSource === 'auto') {
256269
if (hookData.cost?.total_cost_usd != null) {
257-
return hookData.cost.total_cost_usd;
270+
return { sessionCost: hookData.cost.total_cost_usd };
258271
}
259272
// Fallback to ccusage calculation
260-
return Result.pipe(
261-
Result.try({
262-
try: async () => loadSessionUsageById(sessionId, {
263-
mode: 'auto',
264-
offline: mergedOptions.offline,
265-
}),
266-
catch: error => error,
267-
})(),
268-
Result.map(sessionCost => sessionCost?.totalCost),
269-
Result.inspectError(error => logger.error('Failed to load session data:', error)),
270-
Result.unwrap(undefined),
271-
);
273+
const cost = await getCcusageCost();
274+
return { sessionCost: cost };
272275
}
273276
costSource satisfies never; // Exhaustiveness check
277+
return {}; // This line should never be reached
274278
})();
275279

276280
// Load today's usage data
@@ -414,7 +418,16 @@ export const statuslineCommand = define({
414418

415419
// Format and output the status line
416420
// Format: 🤖 model | 💰 session / today / block | 🔥 burn | 🧠 context
417-
const sessionDisplay = sessionCost != null ? formatCurrency(sessionCost) : 'N/A';
421+
const sessionDisplay = (() => {
422+
// If both costs are available, show them side by side
423+
if (ccCost != null || ccusageCost != null) {
424+
const ccDisplay = ccCost != null ? formatCurrency(ccCost) : 'N/A';
425+
const ccusageDisplay = ccusageCost != null ? formatCurrency(ccusageCost) : 'N/A';
426+
return `(${ccDisplay} cc / ${ccusageDisplay} ccusage)`;
427+
}
428+
// Single cost display
429+
return sessionCost != null ? formatCurrency(sessionCost) : 'N/A';
430+
})();
418431
const statusLine = `🤖 ${modelName} | 💰 ${sessionDisplay} session / ${formatCurrency(todayCost)} today / ${blockInfo}${burnRateInfo} | 🧠 ${contextInfo ?? 'N/A'}`;
419432
return statusLine;
420433
},

test/statusline-test-opus4.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,17 @@
99
"workspace": {
1010
"current_dir": "/Users/test/project",
1111
"project_dir": "/Users/test/project"
12-
}
12+
},
13+
"version": "1.0.88",
14+
"output_style": {
15+
"name": "default"
16+
},
17+
"cost": {
18+
"total_cost_usd": 0.0892,
19+
"total_duration_ms": 180000,
20+
"total_api_duration_ms": 12000,
21+
"total_lines_added": 0,
22+
"total_lines_removed": 0
23+
},
24+
"exceeds_200k_tokens": false
1325
}

test/statusline-test-sonnet4.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,17 @@
99
"workspace": {
1010
"current_dir": "/Users/test/project",
1111
"project_dir": "/Users/test/project"
12-
}
12+
},
13+
"version": "1.0.88",
14+
"output_style": {
15+
"name": "default"
16+
},
17+
"cost": {
18+
"total_cost_usd": 0.0245,
19+
"total_duration_ms": 120000,
20+
"total_api_duration_ms": 8500,
21+
"total_lines_added": 0,
22+
"total_lines_removed": 0
23+
},
24+
"exceeds_200k_tokens": false
1325
}

test/statusline-test-sonnet41.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,17 @@
99
"workspace": {
1010
"current_dir": "/Users/test/project",
1111
"project_dir": "/Users/test/project"
12-
}
12+
},
13+
"version": "1.0.88",
14+
"output_style": {
15+
"name": "default"
16+
},
17+
"cost": {
18+
"total_cost_usd": 0.0356,
19+
"total_duration_ms": 150000,
20+
"total_api_duration_ms": 9500,
21+
"total_lines_added": 0,
22+
"total_lines_removed": 0
23+
},
24+
"exceeds_200k_tokens": false
1325
}

test/statusline-test.json

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
{
2-
"session_id": "test-session-123",
3-
"transcript_path": "test/test-transcript.jsonl",
4-
"cwd": "/Users/test/project",
2+
"session_id": "73cc9f9a-2775-4418-beec-bc36b62a1c6f",
3+
"transcript_path": "/Users/ryoppippi/.config/claude/projects/-Users-ryoppippi-ghq-github-com-ryoppippi-ccusage/73cc9f9a-2775-4418-beec-bc36b62a1c6f.jsonl",
4+
"cwd": "/Users/ryoppippi/ghq/github.com/ryoppippi/ccusage",
55
"model": {
6-
"id": "claude-opus-4-1-20250805",
7-
"display_name": "Opus 4.1"
6+
"id": "claude-sonnet-4-20250514",
7+
"display_name": "Sonnet 4"
88
},
99
"workspace": {
10-
"current_dir": "/Users/test/project",
11-
"project_dir": "/Users/test/project"
12-
}
10+
"current_dir": "/Users/ryoppippi/ghq/github.com/ryoppippi/ccusage",
11+
"project_dir": "/Users/ryoppippi/ghq/github.com/ryoppippi/ccusage"
12+
},
13+
"version": "1.0.88",
14+
"output_style": {
15+
"name": "default"
16+
},
17+
"cost": {
18+
"total_cost_usd": 0.056266149999999994,
19+
"total_duration_ms": 164055,
20+
"total_api_duration_ms": 13577,
21+
"total_lines_added": 0,
22+
"total_lines_removed": 0
23+
},
24+
"exceeds_200k_tokens": false
1325
}

0 commit comments

Comments
 (0)