Skip to content

Commit d7a6f8d

Browse files
authored
Merge pull request #166 from rajbos/lastmonth
Show last month stats next to this month
2 parents cf0ce85 + 7738d9c commit d7a6f8d

File tree

4 files changed

+174
-113
lines changed

4 files changed

+174
-113
lines changed

src/extension.ts

Lines changed: 109 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -36,31 +36,23 @@ interface EditorUsage {
3636
};
3737
}
3838

39+
interface PeriodStats {
40+
tokens: number;
41+
sessions: number;
42+
avgInteractionsPerSession: number;
43+
avgTokensPerSession: number;
44+
modelUsage: ModelUsage;
45+
editorUsage: EditorUsage;
46+
co2: number;
47+
treesEquivalent: number;
48+
waterUsage: number;
49+
estimatedCost: number;
50+
}
51+
3952
interface DetailedStats {
40-
today: {
41-
tokens: number;
42-
sessions: number;
43-
avgInteractionsPerSession: number;
44-
avgTokensPerSession: number;
45-
modelUsage: ModelUsage;
46-
editorUsage: EditorUsage;
47-
co2: number;
48-
treesEquivalent: number;
49-
waterUsage: number;
50-
estimatedCost: number;
51-
};
52-
month: {
53-
tokens: number;
54-
sessions: number;
55-
avgInteractionsPerSession: number;
56-
avgTokensPerSession: number;
57-
modelUsage: ModelUsage;
58-
editorUsage: EditorUsage;
59-
co2: number;
60-
treesEquivalent: number;
61-
waterUsage: number;
62-
estimatedCost: number;
63-
};
53+
today: PeriodStats;
54+
month: PeriodStats;
55+
lastMonth: PeriodStats;
6456
lastUpdated: Date;
6557
}
6658

@@ -368,7 +360,9 @@ class CopilotTokenTracker implements vscode.Disposable {
368360

369361
public async clearCache(): Promise<void> {
370362
try {
371-
this.log('[DEBUG] clearCache() called');
363+
// Show the output channel so users can see what's happening
364+
this.outputChannel.show(true);
365+
this.log('DEBUG clearCache() called');
372366
this.log('Clearing session file cache...');
373367

374368
const cacheSize = this.sessionFileCache.size;
@@ -377,6 +371,8 @@ class CopilotTokenTracker implements vscode.Disposable {
377371
// Reset diagnostics loaded flag so the diagnostics view will reload files
378372
this.diagnosticsHasLoadedFiles = false;
379373
this.diagnosticsCachedFiles = [];
374+
// Clear cached computed stats so details panel doesn't show stale data
375+
this.lastDetailedStats = undefined;
380376

381377
this.log(`Cache cleared successfully. Removed ${cacheSize} entries.`);
382378
vscode.window.showInformationMessage('Cache cleared successfully. Reloading statistics...');
@@ -596,9 +592,13 @@ class CopilotTokenTracker implements vscode.Disposable {
596592
const now = new Date();
597593
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
598594
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
595+
// Calculate last month boundaries
596+
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); // Last day of previous month
597+
const lastMonthStart = new Date(lastMonthEnd.getFullYear(), lastMonthEnd.getMonth(), 1);
599598

600599
const todayStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage, editorUsage: {} as EditorUsage };
601600
const monthStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage, editorUsage: {} as EditorUsage };
601+
const lastMonthStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage, editorUsage: {} as EditorUsage };
602602

603603
try {
604604
// Clean expired cache entries
@@ -626,9 +626,9 @@ class CopilotTokenTracker implements vscode.Disposable {
626626
// Fast check: Get file stats first to avoid processing old files
627627
const fileStats = fs.statSync(sessionFile);
628628

629-
// Skip files modified before the current month (quick filter)
629+
// Skip files modified before last month (quick filter)
630630
// This is the main performance optimization - filters out old sessions without reading file content
631-
if (fileStats.mtime < monthStart) {
631+
if (fileStats.mtime < lastMonthStart) {
632632
skippedFiles++;
633633
continue;
634634
}
@@ -708,18 +708,46 @@ class CopilotTokenTracker implements vscode.Disposable {
708708
}
709709
}
710710
}
711+
else if (lastActivity >= lastMonthStart && lastActivity <= lastMonthEnd) {
712+
// Session is from last month - only track lastMonth stats
713+
if (wasCached) {
714+
cacheHits++;
715+
} else {
716+
cacheMisses++;
717+
}
718+
719+
lastMonthStats.tokens += tokens;
720+
lastMonthStats.sessions += 1;
721+
lastMonthStats.interactions += interactions;
722+
723+
// Add editor usage to last month stats
724+
if (!lastMonthStats.editorUsage[editorType]) {
725+
lastMonthStats.editorUsage[editorType] = { tokens: 0, sessions: 0 };
726+
}
727+
lastMonthStats.editorUsage[editorType].tokens += tokens;
728+
lastMonthStats.editorUsage[editorType].sessions += 1;
729+
730+
// Add model usage to last month stats
731+
for (const [model, usage] of Object.entries(modelUsage)) {
732+
if (!lastMonthStats.modelUsage[model]) {
733+
lastMonthStats.modelUsage[model] = { inputTokens: 0, outputTokens: 0 };
734+
}
735+
lastMonthStats.modelUsage[model].inputTokens += usage.inputTokens;
736+
lastMonthStats.modelUsage[model].outputTokens += usage.outputTokens;
737+
}
738+
}
711739
else {
712-
// Session is too old (no activity in current month), skip it
740+
// Session is too old (no activity in current or last month), skip it
713741
skippedFiles++;
714742
}
715743
} catch (fileError) {
716744
this.warn(`Error processing session file ${sessionFile}: ${fileError}`);
717745
}
718746
}
719747

720-
this.log(`✅ Analysis complete: Today ${todayStats.sessions} sessions, Month ${monthStats.sessions} sessions`);
748+
this.log(`✅ Analysis complete: Today ${todayStats.sessions} sessions, Month ${monthStats.sessions} sessions, Last Month ${lastMonthStats.sessions} sessions`);
721749
if (skippedFiles > 0) {
722-
this.log(`⏭️ Skipped ${skippedFiles} session file(s) (empty or no activity in current month)`);
750+
this.log(`⏭️ Skipped ${skippedFiles} session file(s) (empty or no activity in recent months)`);
723751
}
724752
const totalCacheAccesses = cacheHits + cacheMisses;
725753
this.log(`💾 Cache performance: ${cacheHits} hits, ${cacheMisses} misses (${totalCacheAccesses > 0 ? ((cacheHits / totalCacheAccesses) * 100).toFixed(1) : 0}% hit rate)`);
@@ -729,12 +757,15 @@ class CopilotTokenTracker implements vscode.Disposable {
729757

730758
const todayCo2 = (todayStats.tokens / 1000) * this.co2Per1kTokens;
731759
const monthCo2 = (monthStats.tokens / 1000) * this.co2Per1kTokens;
760+
const lastMonthCo2 = (lastMonthStats.tokens / 1000) * this.co2Per1kTokens;
732761

733762
const todayWater = (todayStats.tokens / 1000) * this.waterUsagePer1kTokens;
734763
const monthWater = (monthStats.tokens / 1000) * this.waterUsagePer1kTokens;
764+
const lastMonthWater = (lastMonthStats.tokens / 1000) * this.waterUsagePer1kTokens;
735765

736766
const todayCost = this.calculateEstimatedCost(todayStats.modelUsage);
737767
const monthCost = this.calculateEstimatedCost(monthStats.modelUsage);
768+
const lastMonthCost = this.calculateEstimatedCost(lastMonthStats.modelUsage);
738769

739770
const result: DetailedStats = {
740771
today: {
@@ -761,6 +792,18 @@ class CopilotTokenTracker implements vscode.Disposable {
761792
waterUsage: monthWater,
762793
estimatedCost: monthCost
763794
},
795+
lastMonth: {
796+
tokens: lastMonthStats.tokens,
797+
sessions: lastMonthStats.sessions,
798+
avgInteractionsPerSession: lastMonthStats.sessions > 0 ? Math.round(lastMonthStats.interactions / lastMonthStats.sessions) : 0,
799+
avgTokensPerSession: lastMonthStats.sessions > 0 ? Math.round(lastMonthStats.tokens / lastMonthStats.sessions) : 0,
800+
modelUsage: lastMonthStats.modelUsage,
801+
editorUsage: lastMonthStats.editorUsage,
802+
co2: lastMonthCo2,
803+
treesEquivalent: lastMonthCo2 / this.co2AbsorptionPerTreePerYear,
804+
waterUsage: lastMonthWater,
805+
estimatedCost: lastMonthCost
806+
},
764807
lastUpdated: now
765808
};
766809

@@ -773,7 +816,8 @@ class CopilotTokenTracker implements vscode.Disposable {
773816

774817
private async calculateDailyStats(): Promise<DailyTokenStats[]> {
775818
const now = new Date();
776-
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
819+
// Use last 30 days instead of current month for better chart visibility
820+
const thirtyDaysAgo = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30);
777821

778822
// Map to store daily stats by date string (YYYY-MM-DD)
779823
const dailyStatsMap = new Map<string, DailyTokenStats>();
@@ -786,8 +830,8 @@ class CopilotTokenTracker implements vscode.Disposable {
786830
try {
787831
const fileStats = fs.statSync(sessionFile);
788832

789-
// Only process files modified in the current month
790-
if (fileStats.mtime >= monthStart) {
833+
// Only process files modified in the last 30 days
834+
if (fileStats.mtime >= thirtyDaysAgo) {
791835
const tokens = await this.estimateTokensFromSessionCached(sessionFile, fileStats.mtime.getTime());
792836
const interactions = await this.countInteractionsInSessionCached(sessionFile, fileStats.mtime.getTime());
793837
const modelUsage = await this.getModelUsageFromSessionCached(sessionFile, fileStats.mtime.getTime());
@@ -840,42 +884,39 @@ class CopilotTokenTracker implements vscode.Disposable {
840884
// Convert map to array and sort by date
841885
let dailyStatsArray = Array.from(dailyStatsMap.values()).sort((a, b) => a.date.localeCompare(b.date));
842886

843-
// Fill in missing dates between the first date and today
844-
if (dailyStatsArray.length > 0) {
845-
const firstDate = new Date(dailyStatsArray[0].date);
846-
const today = new Date();
847-
848-
// Create a set of existing dates for quick lookup
849-
const existingDates = new Set(dailyStatsArray.map(s => s.date));
850-
851-
// Generate all dates from first date to today
852-
const allDates: string[] = [];
853-
const currentDate = new Date(firstDate);
854-
855-
while (currentDate <= today) {
856-
const dateKey = this.formatDateKey(currentDate);
857-
allDates.push(dateKey);
858-
currentDate.setDate(currentDate.getDate() + 1);
859-
}
860-
861-
// Add missing dates with zero values
862-
for (const dateKey of allDates) {
863-
if (!existingDates.has(dateKey)) {
864-
dailyStatsMap.set(dateKey, {
865-
date: dateKey,
866-
tokens: 0,
867-
sessions: 0,
868-
interactions: 0,
869-
modelUsage: {},
870-
editorUsage: {}
871-
});
872-
}
887+
// Always fill in all 30 days to show complete chart
888+
const today = new Date();
889+
890+
// Create a set of existing dates for quick lookup
891+
const existingDates = new Set(dailyStatsArray.map(s => s.date));
892+
893+
// Generate all dates from 30 days ago to today
894+
const allDates: string[] = [];
895+
const currentDate = new Date(thirtyDaysAgo);
896+
897+
while (currentDate <= today) {
898+
const dateKey = this.formatDateKey(currentDate);
899+
allDates.push(dateKey);
900+
currentDate.setDate(currentDate.getDate() + 1);
901+
}
902+
903+
// Add missing dates with zero values
904+
for (const dateKey of allDates) {
905+
if (!existingDates.has(dateKey)) {
906+
dailyStatsMap.set(dateKey, {
907+
date: dateKey,
908+
tokens: 0,
909+
sessions: 0,
910+
interactions: 0,
911+
modelUsage: {},
912+
editorUsage: {}
913+
});
873914
}
874-
875-
// Re-convert map to array and sort by date
876-
dailyStatsArray = Array.from(dailyStatsMap.values()).sort((a, b) => a.date.localeCompare(b.date));
877915
}
878916

917+
// Re-convert map to array and sort by date
918+
dailyStatsArray = Array.from(dailyStatsMap.values()).sort((a, b) => a.date.localeCompare(b.date));
919+
879920
return dailyStatsArray;
880921
}
881922

@@ -3192,7 +3233,7 @@ class CopilotTokenTracker implements vscode.Disposable {
31923233

31933234
// Handle messages from the webview
31943235
this.diagnosticsPanel.webview.onDidReceiveMessage(async (message) => {
3195-
this.log(`Diagnostics webview message: ${JSON.stringify(message)}`);
3236+
this.log(`DEBUG Diagnostics webview message: ${JSON.stringify(message)}`);
31963237
switch (message.command) {
31973238
case 'copyReport':
31983239
await vscode.env.clipboard.writeText(report);
@@ -3252,7 +3293,7 @@ class CopilotTokenTracker implements vscode.Disposable {
32523293
await this.showUsageAnalysis();
32533294
break;
32543295
case 'clearCache':
3255-
this.log('[DEBUG] clearCache message received from diagnostics webview');
3296+
this.log('DEBUG clearCache message received from diagnostics webview');
32563297
await this.clearCache();
32573298
// After clearing cache, refresh the diagnostic report if it's open
32583299
if (this.diagnosticsPanel) {

src/webview/chart/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ function renderLayout(data: InitialChartData): void {
9292
const header = el('div', 'header');
9393
const headerLeft = el('div', 'header-left');
9494
const icon = el('span', 'header-icon', '📈');
95-
const title = el('span', 'header-title', 'Token Usage Over Time');
95+
const title = el('span', 'header-title', 'Token Usage - Last 30 Days');
9696
headerLeft.append(icon, title);
9797
const buttons = el('div', 'button-row');
9898
buttons.append(
@@ -140,7 +140,7 @@ function renderLayout(data: InitialChartData): void {
140140
chartShell.append(toggles, canvasWrap);
141141
chartSection.append(chartShell);
142142

143-
const footer = el('div', 'footer', `Day-by-day token usage for the current month\nLast updated: ${new Date(data.lastUpdated).toLocaleString()}\nUpdates automatically every 5 minutes.`);
143+
const footer = el('div', 'footer', `Day-by-day token usage for the last 30 days\nLast updated: ${new Date(data.lastUpdated).toLocaleString()}\nUpdates automatically every 5 minutes.`);
144144

145145
container.append(header, summarySection, chartSection, footer);
146146
root.append(style, container);

0 commit comments

Comments
 (0)