diff --git a/src/webview.ts b/src/webview.ts
index d0531a5b..9ac7b465 100644
--- a/src/webview.ts
+++ b/src/webview.ts
@@ -1416,7 +1416,7 @@ export class UsageWebviewProvider {
${this.renderCompositionChart(
[...this.dailyDataForAllTime]
.sort((a, b) => a.date.localeCompare(b.date))
- .map((d) => ({ label: this.getShortDate(d.date), data: d.data, key: d.date }))
+ .map((d) => ({ label: this.getShortDate(d.date, true), data: d.data, key: d.date }))
)}
@@ -1439,7 +1439,7 @@ export class UsageWebviewProvider {
.map(
({ date, data }) => `
- | ${this.formatDate(date)} |
+ ${this.formatDate(date, true)} |
${I18n.formatCurrency(data.totalCost)} |
${I18n.formatNumber(data.totalInputTokens)} |
${I18n.formatNumber(data.totalOutputTokens)} |
@@ -3228,7 +3228,7 @@ export class UsageWebviewProvider {
private renderAllTimeChart(): string {
const sortedData = [...this.dailyDataForAllTime].sort((a, b) => a.date.localeCompare(b.date));
- return this.renderMainCostChart(sortedData);
+ return this.renderMainCostChart(sortedData, true);
}
/**
@@ -3266,7 +3266,7 @@ export class UsageWebviewProvider {
);
}
- private renderMainCostChart(sortedData: { date: string; data: UsageData }[]): string {
+ private renderMainCostChart(sortedData: { date: string; data: UsageData }[], monthly = false): string {
if (sortedData.length === 0) {
return 'No data available
';
}
@@ -3293,7 +3293,7 @@ export class UsageWebviewProvider {
'data-cost-output="' + cb.output + '" ' +
'data-cost-cachewrite="' + cb.cacheWrite + '" ' +
'data-cost-cacheread="' + cb.cacheRead + '" ' +
- 'title="' + this.escapeHtml(this.formatDate(date)) + ': ' + I18n.formatCurrency(data.totalCost) + '">' +
+ 'title="' + this.escapeHtml(this.formatDate(date, monthly)) + ': ' + I18n.formatCurrency(data.totalCost) + '">' +
costStack(data, barHeight) +
'' +
''
@@ -3302,7 +3302,7 @@ export class UsageWebviewProvider {
.join('');
const xlabels = sortedData
- .map(({ date }) => '' + this.getShortDate(date) + '
')
+ .map(({ date }) => '' + this.getShortDate(date, monthly) + '
')
.join('');
return (
@@ -3387,25 +3387,27 @@ export class UsageWebviewProvider {
);
}
- private getShortDate(dateString: string): string {
+ private getShortDate(dateString: string, monthly = false): string {
// Parse 'YYYY-MM-DD' textually — new Date('YYYY-MM-DD') is UTC midnight,
// which shifts the displayed day back by one in negative-UTC timezones.
const [y, m, d] = dateString.split('-').map(Number);
- // Month-only dates (first of month) label as YYYY/MM for monthly charts.
- if (dateString.endsWith('-01')) {
+ // Month-only dates label as YYYY/MM for monthly charts.
+ if (monthly) {
return `${y}/${String(m).padStart(2, '0')}`;
}
return `${m}/${d}`;
}
- private formatDate(dateString: string): string {
- const date = new Date(dateString);
- // Check if this is a month-only date (ends with -01)
- if (dateString.endsWith('-01')) {
- return date.toLocaleDateString(I18n.getLocale(), I18n.dateFormatOptions({ year: 'numeric', month: 'long' }));
+ private formatDate(dateString: string, monthly = false): string {
+ // Parse textually to avoid UTC-midnight timezone shift (new Date('YYYY-MM-DD')
+ // is UTC midnight, which rolls back a day in negative-UTC timezones). The
+ // date keys are already timezone-correct from dayKeyInZone / monthKeyInZone.
+ const [y, m, d] = dateString.split('-').map(Number);
+ const date = new Date(Date.UTC(y, m - 1, d));
+ if (monthly) {
+ return date.toLocaleDateString(I18n.getLocale(), { year: 'numeric', month: 'long', timeZone: 'UTC' });
}
- // Standard date formatting for daily data, locale + timezone aware.
- return date.toLocaleDateString(I18n.getLocale(), I18n.dateFormatOptions());
+ return date.toLocaleDateString(I18n.getLocale(), { ...I18n.dateFormatOptions(), timeZone: 'UTC' });
}
private getStyles(): string {