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
5 changes: 5 additions & 0 deletions apps/ccusage/src/_shared-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export const sharedArgs = {
description: 'Force compact mode for narrow displays (better for screenshots)',
default: false,
},
full: {
type: 'boolean',
description: 'Show all columns at full width without truncation (allows horizontal overflow)',
default: false,
},
} as const satisfies Args;

/**
Expand Down
8 changes: 6 additions & 2 deletions apps/ccusage/src/commands/daily.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { UsageReportConfig } from '@ccusage/terminal/table';
import process from 'node:process';
import {
addEmptySeparatorRow,
calculateCacheHitRate,
createUsageReportTable,
formatTotalsRow,
formatUsageDataRow,
Expand Down Expand Up @@ -113,6 +114,7 @@ export const dailyCommand = define({
outputTokens: data.outputTokens,
cacheCreationTokens: data.cacheCreationTokens,
cacheReadTokens: data.cacheReadTokens,
cacheHitRate: calculateCacheHitRate(data),
totalTokens: getTotalTokens(data),
totalCost: data.totalCost,
modelsUsed: data.modelsUsed,
Expand Down Expand Up @@ -143,6 +145,7 @@ export const dailyCommand = define({
dateFormatter: (dateStr: string) =>
formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? undefined),
forceCompact: ctx.values.compact,
noTruncate: ctx.values.full,
};
const table = createUsageReportTable(tableConfig);

Expand All @@ -156,7 +159,7 @@ export const dailyCommand = define({
// Add project section header
if (!isFirstProject) {
// Add empty row for visual separation between projects
table.push(['', '', '', '', '', '', '', '']);
table.push(['', '', '', '', '', '', '', '', '']);
}

// Add project header row
Expand All @@ -169,6 +172,7 @@ export const dailyCommand = define({
'',
'',
'',
'',
]);

// Add data rows for this project
Expand Down Expand Up @@ -213,7 +217,7 @@ export const dailyCommand = define({
}

// Add empty row for visual separation before totals
addEmptySeparatorRow(table, 8);
addEmptySeparatorRow(table, 9);

// Add totals
const totalsRow = formatTotalsRow({
Expand Down
5 changes: 4 additions & 1 deletion apps/ccusage/src/commands/monthly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { UsageReportConfig } from '@ccusage/terminal/table';
import process from 'node:process';
import {
addEmptySeparatorRow,
calculateCacheHitRate,
createUsageReportTable,
formatTotalsRow,
formatUsageDataRow,
Expand Down Expand Up @@ -74,6 +75,7 @@ export const monthlyCommand = define({
outputTokens: data.outputTokens,
cacheCreationTokens: data.cacheCreationTokens,
cacheReadTokens: data.cacheReadTokens,
cacheHitRate: calculateCacheHitRate(data),
totalTokens: getTotalTokens(data),
totalCost: data.totalCost,
modelsUsed: data.modelsUsed,
Expand Down Expand Up @@ -107,6 +109,7 @@ export const monthlyCommand = define({
mergedOptions.locale ?? DEFAULT_LOCALE,
),
forceCompact: ctx.values.compact,
noTruncate: ctx.values.full,
};
const table = createUsageReportTable(tableConfig);

Expand All @@ -130,7 +133,7 @@ export const monthlyCommand = define({
}

// Add empty row for visual separation before totals
addEmptySeparatorRow(table, 8);
addEmptySeparatorRow(table, 9);

// Add totals
const totalsRow = formatTotalsRow({
Expand Down
5 changes: 4 additions & 1 deletion apps/ccusage/src/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { UsageReportConfig } from '@ccusage/terminal/table';
import process from 'node:process';
import {
addEmptySeparatorRow,
calculateCacheHitRate,
createUsageReportTable,
formatTotalsRow,
formatUsageDataRow,
Expand Down Expand Up @@ -101,6 +102,7 @@ export const sessionCommand = define({
outputTokens: data.outputTokens,
cacheCreationTokens: data.cacheCreationTokens,
cacheReadTokens: data.cacheReadTokens,
cacheHitRate: calculateCacheHitRate(data),
totalTokens: getTotalTokens(data),
totalCost: data.totalCost,
lastActivity: data.lastActivity,
Expand Down Expand Up @@ -133,6 +135,7 @@ export const sessionCommand = define({
dateFormatter: (dateStr: string) =>
formatDateCompact(dateStr, ctx.values.timezone, ctx.values.locale),
forceCompact: ctx.values.compact,
noTruncate: ctx.values.full,
};
const table = createUsageReportTable(tableConfig);

Expand Down Expand Up @@ -166,7 +169,7 @@ export const sessionCommand = define({
}

// Add empty row for visual separation before totals
addEmptySeparatorRow(table, 9);
addEmptySeparatorRow(table, 10);

// Add totals
const totalsRow = formatTotalsRow(
Expand Down
5 changes: 4 additions & 1 deletion apps/ccusage/src/commands/weekly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { UsageReportConfig } from '@ccusage/terminal/table';
import process from 'node:process';
import {
addEmptySeparatorRow,
calculateCacheHitRate,
createUsageReportTable,
formatTotalsRow,
formatUsageDataRow,
Expand Down Expand Up @@ -84,6 +85,7 @@ export const weeklyCommand = define({
outputTokens: data.outputTokens,
cacheCreationTokens: data.cacheCreationTokens,
cacheReadTokens: data.cacheReadTokens,
cacheHitRate: calculateCacheHitRate(data),
totalTokens: getTotalTokens(data),
totalCost: data.totalCost,
modelsUsed: data.modelsUsed,
Expand Down Expand Up @@ -113,6 +115,7 @@ export const weeklyCommand = define({
dateFormatter: (dateStr: string) =>
formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? undefined),
forceCompact: ctx.values.compact,
noTruncate: ctx.values.full,
};
const table = createUsageReportTable(tableConfig);

Expand All @@ -136,7 +139,7 @@ export const weeklyCommand = define({
}

// Add empty row for visual separation before totals
addEmptySeparatorRow(table, 8);
addEmptySeparatorRow(table, 9);

// Add totals
const totalsRow = formatTotalsRow({
Expand Down
69 changes: 62 additions & 7 deletions packages/terminal/src/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export type TableOptions = {
compactColAligns?: TableCellAlign[];
compactThreshold?: number;
forceCompact?: boolean;
noTruncate?: boolean;
logger?: (message: string) => void;
};

Expand All @@ -95,6 +96,7 @@ export class ResponsiveTable {
private compactThreshold: number;
private compactMode = false;
private forceCompact: boolean;
private noTruncate: boolean;
private logger: (message: string) => void;

/**
Expand All @@ -110,6 +112,7 @@ export class ResponsiveTable {
this.compactColAligns = options.compactColAligns;
this.compactThreshold = options.compactThreshold ?? 100;
this.forceCompact = options.forceCompact ?? false;
this.noTruncate = options.noTruncate ?? false;
this.logger = options.logger ?? console.warn;
}

Expand Down Expand Up @@ -183,9 +186,10 @@ export class ResponsiveTable {
const terminalWidth =
Number.parseInt(process.env.COLUMNS ?? '', 10) || process.stdout.columns || 120;

// Determine if we should use compact mode
// Determine if we should use compact mode (--full overrides compact)
this.compactMode =
this.forceCompact || (terminalWidth < this.compactThreshold && this.compactHead != null);
!this.noTruncate &&
(this.forceCompact || (terminalWidth < this.compactThreshold && this.compactHead != null));

// Get current table configuration
const { head, colAligns } = this.getCurrentTableConfig();
Expand Down Expand Up @@ -237,7 +241,7 @@ export class ResponsiveTable {
// Check if this fits in the terminal
const totalRequiredWidth = columnWidths.reduce((sum, width) => sum + width, 0) + tableOverhead;

if (totalRequiredWidth > terminalWidth) {
if (totalRequiredWidth > terminalWidth && !this.noTruncate) {
// Apply responsive resizing and use compact date format if available
const scaleFactor = availableWidth / columnWidths.reduce((sum, width) => sum + width, 0);
const adjustedWidths = columnWidths.map((width, index) => {
Expand Down Expand Up @@ -303,8 +307,8 @@ export class ResponsiveTable {
style: this.style,
colAligns,
colWidths: columnWidths,
wordWrap: true,
wrapOnWordBoundary: true,
wordWrap: !this.noTruncate,
wrapOnWordBoundary: !this.noTruncate,
});

// Add rows with special handling for separators
Expand Down Expand Up @@ -475,6 +479,7 @@ export function pushBreakdownRows(
pc.gray(formatNumber(breakdown.outputTokens)),
pc.gray(formatNumber(breakdown.cacheCreationTokens)),
pc.gray(formatNumber(breakdown.cacheReadTokens)),
pc.gray(formatCacheHitRate(breakdown)),
pc.gray(formatNumber(totalTokens)),
pc.gray(formatCurrency(breakdown.cost)),
);
Expand All @@ -500,6 +505,8 @@ export type UsageReportConfig = {
dateFormatter?: (dateStr: string) => string;
/** Force compact mode regardless of terminal width */
forceCompact?: boolean;
/** Disable column truncation/scaling */
noTruncate?: boolean;
};

/**
Expand All @@ -514,6 +521,42 @@ export type UsageData = {
modelsUsed?: string[];
};

/**
* Calculates and formats cache hit rate with color coding
* @param data - Usage data containing token counts
* @returns Color-coded percentage string
*/
export function formatCacheHitRate(data: {
inputTokens: number;
cacheCreationTokens: number;
cacheReadTokens: number;
}): string {
const totalInput = data.inputTokens + data.cacheCreationTokens + data.cacheReadTokens;
const rate = totalInput > 0 ? data.cacheReadTokens / totalInput : 0;
const pct = `${(rate * 100).toFixed(1)}%`;
if (rate >= 0.7) {
return pc.green(pct);
}
if (rate >= 0.4) {
return pc.yellow(pct);
}
return pc.red(pct);
}

/**
* Calculates cache hit rate as a number
* @param data - Usage data containing token counts
* @returns Cache hit rate between 0 and 1
*/
export function calculateCacheHitRate(data: {
inputTokens: number;
cacheCreationTokens: number;
cacheReadTokens: number;
}): number {
const totalInput = data.inputTokens + data.cacheCreationTokens + data.cacheReadTokens;
return totalInput > 0 ? data.cacheReadTokens / totalInput : 0;
}

/**
* Creates a standard usage report table with consistent styling and layout
* @param config - Configuration options for the table
Expand All @@ -527,6 +570,7 @@ export function createUsageReportTable(config: UsageReportConfig): ResponsiveTab
'Output',
'Cache Create',
'Cache Read',
'Hit Rate',
'Total Tokens',
'Cost (USD)',
];
Expand All @@ -540,11 +584,19 @@ export function createUsageReportTable(config: UsageReportConfig): ResponsiveTab
'right',
'right',
'right',
'right',
];

const compactHeaders = [config.firstColumnName, 'Models', 'Input', 'Output', 'Cost (USD)'];
const compactHeaders = [
config.firstColumnName,
'Models',
'Input',
'Output',
'Hit Rate',
'Cost (USD)',
];

const compactAligns: TableCellAlign[] = ['left', 'left', 'right', 'right', 'right'];
const compactAligns: TableCellAlign[] = ['left', 'left', 'right', 'right', 'right', 'right'];

// Add Last Activity column for session reports
if (config.includeLastActivity ?? false) {
Expand All @@ -563,6 +615,7 @@ export function createUsageReportTable(config: UsageReportConfig): ResponsiveTab
compactColAligns: compactAligns,
compactThreshold: 100,
forceCompact: config.forceCompact,
noTruncate: config.noTruncate,
});
}

Expand All @@ -588,6 +641,7 @@ export function formatUsageDataRow(
formatNumber(data.outputTokens),
formatNumber(data.cacheCreationTokens),
formatNumber(data.cacheReadTokens),
formatCacheHitRate(data),
formatNumber(totalTokens),
formatCurrency(data.totalCost),
];
Expand Down Expand Up @@ -619,6 +673,7 @@ export function formatTotalsRow(
pc.yellow(formatNumber(totals.outputTokens)),
pc.yellow(formatNumber(totals.cacheCreationTokens)),
pc.yellow(formatNumber(totals.cacheReadTokens)),
pc.yellow(formatCacheHitRate(totals)),
pc.yellow(formatNumber(totalTokens)),
pc.yellow(formatCurrency(totals.totalCost)),
];
Expand Down