Skip to content

Commit 1e99896

Browse files
committed
feat: consolidate common table operations across command files
- Add shared table factory function createUsageReportTable() with consistent headers and styling - Add formatUsageDataRow() for standardized data row formatting with token calculations - Add formatTotalsRow() with yellow highlighting and optional Last Activity column support - Add addEmptySeparatorRow() utility for consistent visual separation - Define UsageReportConfig and UsageData interfaces for better type safety - Refactor daily.ts, monthly.ts, weekly.ts, and session.ts to use shared table functions - Support Last Activity column for session reports via includeLastActivity config option - Reduce code duplication by ~50-70 lines per command file - Maintain all existing functionality and table formatting consistency - Fix ESLint strict boolean expression warnings with nullish coalescing This consolidation improves maintainability by centralizing table creation logic while preserving responsive design and compact mode functionality across all commands.
1 parent ef631c8 commit 1e99896

File tree

5 files changed

+265
-304
lines changed

5 files changed

+265
-304
lines changed

src/_table.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,165 @@ export function pushBreakdownRows(
405405
}
406406
}
407407

408+
/**
409+
* Configuration options for creating usage report tables
410+
*/
411+
export type UsageReportConfig = {
412+
/** Name for the first column (Date, Month, Week, Session, etc.) */
413+
firstColumnName: string;
414+
/** Whether to include Last Activity column (for session reports) */
415+
includeLastActivity?: boolean;
416+
/** Date formatter function for responsive date formatting */
417+
dateFormatter?: (dateStr: string) => string;
418+
/** Force compact mode regardless of terminal width */
419+
forceCompact?: boolean;
420+
};
421+
422+
/**
423+
* Standard usage data structure for table rows
424+
*/
425+
export type UsageData = {
426+
inputTokens: number;
427+
outputTokens: number;
428+
cacheCreationTokens: number;
429+
cacheReadTokens: number;
430+
totalCost: number;
431+
modelsUsed?: string[];
432+
};
433+
434+
/**
435+
* Creates a standard usage report table with consistent styling and layout
436+
* @param config - Configuration options for the table
437+
* @returns Configured ResponsiveTable instance
438+
*/
439+
export function createUsageReportTable(config: UsageReportConfig): ResponsiveTable {
440+
const baseHeaders = [
441+
config.firstColumnName,
442+
'Models',
443+
'Input',
444+
'Output',
445+
'Cache Create',
446+
'Cache Read',
447+
'Total Tokens',
448+
'Cost (USD)',
449+
];
450+
451+
const baseAligns: TableCellAlign[] = [
452+
'left',
453+
'left',
454+
'right',
455+
'right',
456+
'right',
457+
'right',
458+
'right',
459+
'right',
460+
];
461+
462+
const compactHeaders = [
463+
config.firstColumnName,
464+
'Models',
465+
'Input',
466+
'Output',
467+
'Cost (USD)',
468+
];
469+
470+
const compactAligns: TableCellAlign[] = [
471+
'left',
472+
'left',
473+
'right',
474+
'right',
475+
'right',
476+
];
477+
478+
// Add Last Activity column for session reports
479+
if (config.includeLastActivity ?? false) {
480+
baseHeaders.push('Last Activity');
481+
baseAligns.push('left');
482+
compactHeaders.push('Last Activity');
483+
compactAligns.push('left');
484+
}
485+
486+
return new ResponsiveTable({
487+
head: baseHeaders,
488+
style: { head: ['cyan'] },
489+
colAligns: baseAligns,
490+
dateFormatter: config.dateFormatter,
491+
compactHead: compactHeaders,
492+
compactColAligns: compactAligns,
493+
compactThreshold: 100,
494+
forceCompact: config.forceCompact,
495+
});
496+
}
497+
498+
/**
499+
* Formats a usage data row for display in the table
500+
* @param firstColumnValue - Value for the first column (date, month, etc.)
501+
* @param data - Usage data containing tokens and cost information
502+
* @param lastActivity - Optional last activity value (for session reports)
503+
* @returns Formatted table row
504+
*/
505+
export function formatUsageDataRow(
506+
firstColumnValue: string,
507+
data: UsageData,
508+
lastActivity?: string,
509+
): (string | number)[] {
510+
const totalTokens = data.inputTokens + data.outputTokens + data.cacheCreationTokens + data.cacheReadTokens;
511+
512+
const row: (string | number)[] = [
513+
firstColumnValue,
514+
data.modelsUsed != null ? formatModelsDisplayMultiline(data.modelsUsed) : '',
515+
formatNumber(data.inputTokens),
516+
formatNumber(data.outputTokens),
517+
formatNumber(data.cacheCreationTokens),
518+
formatNumber(data.cacheReadTokens),
519+
formatNumber(totalTokens),
520+
formatCurrency(data.totalCost),
521+
];
522+
523+
if (lastActivity !== undefined) {
524+
row.push(lastActivity);
525+
}
526+
527+
return row;
528+
}
529+
530+
/**
531+
* Creates a totals row with yellow highlighting
532+
* @param totals - Totals data to display
533+
* @param includeLastActivity - Whether to include an empty last activity column
534+
* @returns Formatted totals row
535+
*/
536+
export function formatTotalsRow(totals: UsageData, includeLastActivity = false): (string | number)[] {
537+
const totalTokens = totals.inputTokens + totals.outputTokens + totals.cacheCreationTokens + totals.cacheReadTokens;
538+
539+
const row: (string | number)[] = [
540+
pc.yellow('Total'),
541+
'', // Empty for Models column in totals
542+
pc.yellow(formatNumber(totals.inputTokens)),
543+
pc.yellow(formatNumber(totals.outputTokens)),
544+
pc.yellow(formatNumber(totals.cacheCreationTokens)),
545+
pc.yellow(formatNumber(totals.cacheReadTokens)),
546+
pc.yellow(formatNumber(totalTokens)),
547+
pc.yellow(formatCurrency(totals.totalCost)),
548+
];
549+
550+
if (includeLastActivity) {
551+
row.push(''); // Empty for Last Activity column in totals
552+
}
553+
554+
return row;
555+
}
556+
557+
/**
558+
* Adds an empty separator row to the table for visual separation
559+
* @param table - Table to add separator row to
560+
* @param columnCount - Number of columns in the table
561+
*/
562+
export function addEmptySeparatorRow(table: ResponsiveTable, columnCount: number): void {
563+
const emptyRow = Array.from({ length: columnCount }, () => '');
564+
table.push(emptyRow);
565+
}
566+
408567
if (import.meta.vitest != null) {
409568
describe('ResponsiveTable', () => {
410569
describe('compact mode behavior', () => {

src/commands/daily.ts

Lines changed: 33 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { UsageReportConfig } from '../_table.ts';
12
import process from 'node:process';
23
import { Result } from '@praha/byethrow';
34
import { define } from 'gunshi';
@@ -7,7 +8,7 @@ import { groupByProject, groupDataByProject } from '../_daily-grouping.ts';
78
import { processWithJq } from '../_jq-processor.ts';
89
import { formatProjectName } from '../_project-names.ts';
910
import { sharedCommandConfig } from '../_shared-args.ts';
10-
import { formatCurrency, formatModelsDisplayMultiline, formatNumber, pushBreakdownRows, ResponsiveTable } from '../_table.ts';
11+
import { addEmptySeparatorRow, createUsageReportTable, formatTotalsRow, formatUsageDataRow, pushBreakdownRows } from '../_table.ts';
1112
import {
1213
calculateTotals,
1314
createTotalsObject,
@@ -132,48 +133,12 @@ export const dailyCommand = define({
132133
logger.box('Claude Code Token Usage Report - Daily');
133134

134135
// Create table with compact mode support
135-
const table = new ResponsiveTable({
136-
head: [
137-
'Date',
138-
'Models',
139-
'Input',
140-
'Output',
141-
'Cache Create',
142-
'Cache Read',
143-
'Total Tokens',
144-
'Cost (USD)',
145-
],
146-
style: {
147-
head: ['cyan'],
148-
},
149-
colAligns: [
150-
'left',
151-
'left',
152-
'right',
153-
'right',
154-
'right',
155-
'right',
156-
'right',
157-
'right',
158-
],
136+
const tableConfig: UsageReportConfig = {
137+
firstColumnName: 'Date',
159138
dateFormatter: (dateStr: string) => formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? undefined),
160-
compactHead: [
161-
'Date',
162-
'Models',
163-
'Input',
164-
'Output',
165-
'Cost (USD)',
166-
],
167-
compactColAligns: [
168-
'left',
169-
'left',
170-
'right',
171-
'right',
172-
'right',
173-
],
174-
compactThreshold: 100,
175139
forceCompact: ctx.values.compact,
176-
});
140+
};
141+
const table = createUsageReportTable(tableConfig);
177142

178143
// Add daily data - group by project if instances flag is used
179144
if (Boolean(mergedOptions.instances) && dailyData.some(d => d.project != null)) {
@@ -202,16 +167,15 @@ export const dailyCommand = define({
202167

203168
// Add data rows for this project
204169
for (const data of projectData) {
205-
table.push([
206-
data.date,
207-
formatModelsDisplayMultiline(data.modelsUsed),
208-
formatNumber(data.inputTokens),
209-
formatNumber(data.outputTokens),
210-
formatNumber(data.cacheCreationTokens),
211-
formatNumber(data.cacheReadTokens),
212-
formatNumber(getTotalTokens(data)),
213-
formatCurrency(data.totalCost),
214-
]);
170+
const row = formatUsageDataRow(data.date, {
171+
inputTokens: data.inputTokens,
172+
outputTokens: data.outputTokens,
173+
cacheCreationTokens: data.cacheCreationTokens,
174+
cacheReadTokens: data.cacheReadTokens,
175+
totalCost: data.totalCost,
176+
modelsUsed: data.modelsUsed,
177+
});
178+
table.push(row);
215179

216180
// Add model breakdown rows if flag is set
217181
if (mergedOptions.breakdown) {
@@ -226,16 +190,15 @@ export const dailyCommand = define({
226190
// Standard display without project grouping
227191
for (const data of dailyData) {
228192
// Main row
229-
table.push([
230-
data.date,
231-
formatModelsDisplayMultiline(data.modelsUsed),
232-
formatNumber(data.inputTokens),
233-
formatNumber(data.outputTokens),
234-
formatNumber(data.cacheCreationTokens),
235-
formatNumber(data.cacheReadTokens),
236-
formatNumber(getTotalTokens(data)),
237-
formatCurrency(data.totalCost),
238-
]);
193+
const row = formatUsageDataRow(data.date, {
194+
inputTokens: data.inputTokens,
195+
outputTokens: data.outputTokens,
196+
cacheCreationTokens: data.cacheCreationTokens,
197+
cacheReadTokens: data.cacheReadTokens,
198+
totalCost: data.totalCost,
199+
modelsUsed: data.modelsUsed,
200+
});
201+
table.push(row);
239202

240203
// Add model breakdown rows if flag is set
241204
if (mergedOptions.breakdown) {
@@ -245,28 +208,17 @@ export const dailyCommand = define({
245208
}
246209

247210
// Add empty row for visual separation before totals
248-
table.push([
249-
'',
250-
'',
251-
'',
252-
'',
253-
'',
254-
'',
255-
'',
256-
'',
257-
]);
211+
addEmptySeparatorRow(table, 8);
258212

259213
// Add totals
260-
table.push([
261-
pc.yellow('Total'),
262-
'', // Empty for Models column in totals
263-
pc.yellow(formatNumber(totals.inputTokens)),
264-
pc.yellow(formatNumber(totals.outputTokens)),
265-
pc.yellow(formatNumber(totals.cacheCreationTokens)),
266-
pc.yellow(formatNumber(totals.cacheReadTokens)),
267-
pc.yellow(formatNumber(getTotalTokens(totals))),
268-
pc.yellow(formatCurrency(totals.totalCost)),
269-
]);
214+
const totalsRow = formatTotalsRow({
215+
inputTokens: totals.inputTokens,
216+
outputTokens: totals.outputTokens,
217+
cacheCreationTokens: totals.cacheCreationTokens,
218+
cacheReadTokens: totals.cacheReadTokens,
219+
totalCost: totals.totalCost,
220+
});
221+
table.push(totalsRow);
270222

271223
log(table.toString());
272224

0 commit comments

Comments
 (0)