Skip to content
Merged
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
69 changes: 52 additions & 17 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ interface DetailedStats {
today: PeriodStats;
month: PeriodStats;
lastMonth: PeriodStats;
last30Days: PeriodStats;
lastUpdated: Date;
}

Expand Down Expand Up @@ -670,10 +671,13 @@ class CopilotTokenTracker implements vscode.Disposable {
// Calculate last month boundaries
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); // Last day of previous month
const lastMonthStart = new Date(lastMonthEnd.getFullYear(), lastMonthEnd.getMonth(), 1);
// Calculate last 30 days boundary
const last30DaysStart = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);

const todayStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage, editorUsage: {} as EditorUsage };
const monthStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage, editorUsage: {} as EditorUsage };
const lastMonthStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage, editorUsage: {} as EditorUsage };
const last30DaysStats = { tokens: 0, sessions: 0, interactions: 0, modelUsage: {} as ModelUsage, editorUsage: {} as EditorUsage };

try {
// Clean expired cache entries
Expand Down Expand Up @@ -701,14 +705,14 @@ class CopilotTokenTracker implements vscode.Disposable {
// Fast check: Get file stats first to avoid processing old files
const fileStats = fs.statSync(sessionFile);

// Skip files modified before last month (quick filter)
// Skip files modified before last 30 days (quick filter)
// This is the main performance optimization - filters out old sessions without reading file content
if (fileStats.mtime < lastMonthStart) {
if (fileStats.mtime < last30DaysStart) {
skippedFiles++;
continue;
}

// For files within current month, check if data is cached to avoid redundant reads
// For files within last 30 days, check if data is cached to avoid redundant reads
const mtime = fileStats.mtime.getTime();
const fileSize = fileStats.size;
const wasCached = this.isCacheValid(sessionFile, mtime, fileSize);
Expand All @@ -732,15 +736,37 @@ class CopilotTokenTracker implements vscode.Disposable {
? new Date(details.lastInteraction)
: new Date(details.modified);

if (lastActivity >= monthStart) {
// Update cache statistics (do this once per file)
if (wasCached) {
cacheHits++;
} else {
cacheMisses++;
}

// Update cache statistics
if (wasCached) {
cacheHits++;
} else {
cacheMisses++;
// Check if activity is within last 30 days
if (lastActivity >= last30DaysStart) {
last30DaysStats.tokens += tokens;
last30DaysStats.sessions += 1;
last30DaysStats.interactions += interactions;

// Add editor usage to last 30 days stats
if (!last30DaysStats.editorUsage[editorType]) {
last30DaysStats.editorUsage[editorType] = { tokens: 0, sessions: 0 };
}
last30DaysStats.editorUsage[editorType].tokens += tokens;
last30DaysStats.editorUsage[editorType].sessions += 1;

// Add model usage to last 30 days stats
for (const [model, usage] of Object.entries(modelUsage)) {
if (!last30DaysStats.modelUsage[model]) {
last30DaysStats.modelUsage[model] = { inputTokens: 0, outputTokens: 0 };
}
last30DaysStats.modelUsage[model].inputTokens += usage.inputTokens;
last30DaysStats.modelUsage[model].outputTokens += usage.outputTokens;
}
}

if (lastActivity >= monthStart) {
monthStats.tokens += tokens;
monthStats.sessions += 1;
monthStats.interactions += interactions;
Expand Down Expand Up @@ -785,12 +811,6 @@ class CopilotTokenTracker implements vscode.Disposable {
}
else if (lastActivity >= lastMonthStart && lastActivity <= lastMonthEnd) {
// Session is from last month - only track lastMonth stats
if (wasCached) {
cacheHits++;
} else {
cacheMisses++;
}

lastMonthStats.tokens += tokens;
lastMonthStats.sessions += 1;
lastMonthStats.interactions += interactions;
Expand All @@ -812,15 +832,15 @@ class CopilotTokenTracker implements vscode.Disposable {
}
}
else {
// Session is too old (no activity in current or last month), skip it
// Session is too old (no activity in last 30 days), skip it
skippedFiles++;
}
} catch (fileError) {
this.warn(`Error processing session file ${sessionFile}: ${fileError}`);
}
}

this.log(`✅ Analysis complete: Today ${todayStats.sessions} sessions, Month ${monthStats.sessions} sessions, Last Month ${lastMonthStats.sessions} sessions`);
this.log(`✅ Analysis complete: Today ${todayStats.sessions} sessions, Month ${monthStats.sessions} sessions, Last 30 Days ${last30DaysStats.sessions} sessions, Last Month ${lastMonthStats.sessions} sessions`);
if (skippedFiles > 0) {
this.log(`⏭️ Skipped ${skippedFiles} session file(s) (empty or no activity in recent months)`);
}
Expand All @@ -833,14 +853,17 @@ class CopilotTokenTracker implements vscode.Disposable {
const todayCo2 = (todayStats.tokens / 1000) * this.co2Per1kTokens;
const monthCo2 = (monthStats.tokens / 1000) * this.co2Per1kTokens;
const lastMonthCo2 = (lastMonthStats.tokens / 1000) * this.co2Per1kTokens;
const last30DaysCo2 = (last30DaysStats.tokens / 1000) * this.co2Per1kTokens;

const todayWater = (todayStats.tokens / 1000) * this.waterUsagePer1kTokens;
const monthWater = (monthStats.tokens / 1000) * this.waterUsagePer1kTokens;
const lastMonthWater = (lastMonthStats.tokens / 1000) * this.waterUsagePer1kTokens;
const last30DaysWater = (last30DaysStats.tokens / 1000) * this.waterUsagePer1kTokens;

const todayCost = this.calculateEstimatedCost(todayStats.modelUsage);
const monthCost = this.calculateEstimatedCost(monthStats.modelUsage);
const lastMonthCost = this.calculateEstimatedCost(lastMonthStats.modelUsage);
const last30DaysCost = this.calculateEstimatedCost(last30DaysStats.modelUsage);

const result: DetailedStats = {
today: {
Expand Down Expand Up @@ -879,6 +902,18 @@ class CopilotTokenTracker implements vscode.Disposable {
waterUsage: lastMonthWater,
estimatedCost: lastMonthCost
},
last30Days: {
tokens: last30DaysStats.tokens,
sessions: last30DaysStats.sessions,
avgInteractionsPerSession: last30DaysStats.sessions > 0 ? Math.round(last30DaysStats.interactions / last30DaysStats.sessions) : 0,
avgTokensPerSession: last30DaysStats.sessions > 0 ? Math.round(last30DaysStats.tokens / last30DaysStats.sessions) : 0,
modelUsage: last30DaysStats.modelUsage,
editorUsage: last30DaysStats.editorUsage,
co2: last30DaysCo2,
treesEquivalent: last30DaysCo2 / this.co2AbsorptionPerTreePerYear,
waterUsage: last30DaysWater,
estimatedCost: last30DaysCost
},
lastUpdated: now
};

Expand Down
34 changes: 18 additions & 16 deletions src/webview/details/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type DetailedStats = {
today: PeriodStats;
month: PeriodStats;
lastMonth: PeriodStats;
last30Days: PeriodStats;
lastUpdated: string | Date;
};

Expand All @@ -52,25 +53,23 @@ console.log('[CopilotTokenTracker] details webview loaded');
console.log('[CopilotTokenTracker] window.__INITIAL_DETAILS__:', window.__INITIAL_DETAILS__);
console.log('[CopilotTokenTracker] initialData:', initialData);

function calculateProjection(monthValue: number): number {
const now = new Date();
const day = now.getDate();
const isLeap = (now.getFullYear() % 4 === 0 && now.getFullYear() % 100 !== 0) || now.getFullYear() % 400 === 0;
const daysInYear = isLeap ? 366 : 365;
if (day === 0) { return 0; }
return (monthValue / day) * daysInYear;
function calculateProjection(last30DaysValue: number): number {
// Project annual value based on last 30 days average
// This gives better predictions at the beginning of the month
const daysInYear = 365.25; // Average days per year (accounting for leap year cycle)
return (last30DaysValue / 30) * daysInYear;
}

function render(stats: DetailedStats): void {
const root = document.getElementById('root');
if (!root) { return; }

const projectedTokens = Math.round(calculateProjection(stats.month.tokens));
const projectedSessions = Math.round(calculateProjection(stats.month.sessions));
const projectedCo2 = calculateProjection(stats.month.co2);
const projectedWater = calculateProjection(stats.month.waterUsage);
const projectedCost = calculateProjection(stats.month.estimatedCost);
const projectedTrees = calculateProjection(stats.month.treesEquivalent);
const projectedTokens = Math.round(calculateProjection(stats.last30Days.tokens));
const projectedSessions = Math.round(calculateProjection(stats.last30Days.sessions));
const projectedCo2 = calculateProjection(stats.last30Days.co2);
const projectedWater = calculateProjection(stats.last30Days.waterUsage);
const projectedCost = calculateProjection(stats.last30Days.estimatedCost);
const projectedTrees = calculateProjection(stats.last30Days.treesEquivalent);

renderShell(root, stats, {
projectedTokens,
Expand Down Expand Up @@ -307,11 +306,12 @@ function buildEditorUsageSection(stats: DetailedStats): HTMLElement | null {
const todayUsage = stats.today.editorUsage[editor] || { tokens: 0, sessions: 0 };
const monthUsage = stats.month.editorUsage[editor] || { tokens: 0, sessions: 0 };
const lastMonthUsage = stats.lastMonth.editorUsage[editor] || { tokens: 0, sessions: 0 };
const last30DaysUsage = stats.last30Days.editorUsage[editor] || { tokens: 0, sessions: 0 };
const todayPercent = todayTotal > 0 ? (todayUsage.tokens / todayTotal) * 100 : 0;
const monthPercent = monthTotal > 0 ? (monthUsage.tokens / monthTotal) * 100 : 0;
const lastMonthPercent = lastMonthTotal > 0 ? (lastMonthUsage.tokens / lastMonthTotal) * 100 : 0;
const projectedTokens = Math.round(calculateProjection(monthUsage.tokens));
const projectedSessions = Math.round(calculateProjection(monthUsage.sessions));
const projectedTokens = Math.round(calculateProjection(last30DaysUsage.tokens));
const projectedSessions = Math.round(calculateProjection(last30DaysUsage.sessions));

const tr = document.createElement('tr');
const labelTd = document.createElement('td');
Expand Down Expand Up @@ -399,10 +399,12 @@ function buildModelUsageSection(stats: DetailedStats): HTMLElement | null {
const todayUsage = stats.today.modelUsage[model] || { inputTokens: 0, outputTokens: 0 };
const monthUsage = stats.month.modelUsage[model] || { inputTokens: 0, outputTokens: 0 };
const lastMonthUsage = stats.lastMonth.modelUsage[model] || { inputTokens: 0, outputTokens: 0 };
const last30DaysUsage = stats.last30Days.modelUsage[model] || { inputTokens: 0, outputTokens: 0 };
const todayTotal = todayUsage.inputTokens + todayUsage.outputTokens;
const monthTotal = monthUsage.inputTokens + monthUsage.outputTokens;
const lastMonthTotal = lastMonthUsage.inputTokens + lastMonthUsage.outputTokens;
const projected = Math.round(calculateProjection(monthTotal));
const last30DaysTotal = last30DaysUsage.inputTokens + last30DaysUsage.outputTokens;
const projected = Math.round(calculateProjection(last30DaysTotal));
const todayInputPct = todayTotal > 0 ? (todayUsage.inputTokens / todayTotal) * 100 : 0;
const todayOutputPct = todayTotal > 0 ? (todayUsage.outputTokens / todayTotal) * 100 : 0;
const monthInputPct = monthTotal > 0 ? (monthUsage.inputTokens / monthTotal) * 100 : 0;
Expand Down
Loading