From d8835de1a3a5306694ca70916aebb9031ee60eb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 07:08:21 +0000 Subject: [PATCH 1/3] Initial plan From 4b7394c4456ab36aad41ff25b9eb0fe9dca3dc6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 07:13:04 +0000 Subject: [PATCH 2/3] Add last30Days period to DetailedStats and update projections to use it Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- package-lock.json | 7 ++++ src/extension.ts | 69 ++++++++++++++++++++++++++++--------- src/webview/details/main.ts | 34 +++++++++--------- 3 files changed, 77 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12ef49a..87ed0b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -563,6 +563,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -604,6 +605,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1894,6 +1896,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -2554,6 +2557,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4036,6 +4040,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8648,6 +8653,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8883,6 +8889,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/extension.ts b/src/extension.ts index 12861b7..fe59d65 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; } @@ -659,10 +660,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 @@ -690,14 +694,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); @@ -721,15 +725,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; @@ -774,12 +800,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; @@ -801,7 +821,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) { @@ -809,7 +829,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)`); } @@ -822,14 +842,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: { @@ -868,6 +891,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..d0dff7b 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; // Account for leap years + 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; From 5041db4291c61231f34d0bbaa967a52f88d833de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 07:15:25 +0000 Subject: [PATCH 3/3] Improve comment clarity for leap year calculation Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- src/webview/details/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webview/details/main.ts b/src/webview/details/main.ts index d0dff7b..19252db 100644 --- a/src/webview/details/main.ts +++ b/src/webview/details/main.ts @@ -56,7 +56,7 @@ console.log('[CopilotTokenTracker] initialData:', initialData); 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; // Account for leap years + const daysInYear = 365.25; // Average days per year (accounting for leap year cycle) return (last30DaysValue / 30) * daysInYear; }