diff --git a/src/extension.ts b/src/extension.ts index 2a71349..da7c837 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -55,6 +55,7 @@ interface DetailedStats { today: PeriodStats; month: PeriodStats; lastMonth: PeriodStats; + last30Days: PeriodStats; lastUpdated: Date; } @@ -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 @@ -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); @@ -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; @@ -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; @@ -812,7 +832,7 @@ 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) { @@ -820,7 +840,7 @@ class CopilotTokenTracker implements vscode.Disposable { } } - 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)`); } @@ -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: { @@ -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 }; diff --git a/src/webview/details/main.ts b/src/webview/details/main.ts index 93dcdb7..19252db 100644 --- a/src/webview/details/main.ts +++ b/src/webview/details/main.ts @@ -27,6 +27,7 @@ type DetailedStats = { today: PeriodStats; month: PeriodStats; lastMonth: PeriodStats; + last30Days: PeriodStats; lastUpdated: string | Date; }; @@ -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, @@ -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'); @@ -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;