From a57cfbcd892b56864120f34b80b6558264e2eab2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 14:53:54 +0000 Subject: [PATCH 1/5] Initial plan From 3c36d872a29c525a757c08a311d09932fc859fc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 14:56:09 +0000 Subject: [PATCH 2/5] Initial plan for chart panel feature Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- package-lock.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index daddcf7..8459537 100644 --- a/package-lock.json +++ b/package-lock.json @@ -374,7 +374,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -416,7 +415,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1655,7 +1653,6 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -2293,7 +2290,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3770,7 +3766,6 @@ "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", @@ -8332,7 +8327,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8569,7 +8563,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From bd2c91c3ab77bba56d2f9d924f56d372023411e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 14:58:56 +0000 Subject: [PATCH 3/5] Add chart panel feature for day-by-day token usage visualization Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- package.json | 5 + src/extension.ts | 418 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 422 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index bab4c64..d7b6682 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,11 @@ "command": "copilot-token-tracker.showDetails", "title": "Show Token Usage Details", "category": "Copilot Token Tracker" + }, + { + "command": "copilot-token-tracker.showChart", + "title": "Show Token Usage Chart", + "category": "Copilot Token Tracker" } ] }, diff --git a/src/extension.ts b/src/extension.ts index 439126d..6d5071e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -50,6 +50,13 @@ interface DetailedStats { lastUpdated: Date; } +interface DailyTokenStats { + date: string; // YYYY-MM-DD format + tokens: number; + sessions: number; + interactions: number; +} + interface SessionFileCache { tokens: number; interactions: number; @@ -67,6 +74,7 @@ class CopilotTokenTracker implements vscode.Disposable { private updateInterval: NodeJS.Timeout | undefined; private initialDelayTimeout: NodeJS.Timeout | undefined; private detailsPanel: vscode.WebviewPanel | undefined; + private chartPanel: vscode.WebviewPanel | undefined; private outputChannel: vscode.OutputChannel; private sessionFileCache: Map = new Map(); private tokenEstimators: { [key: string]: number } = tokenEstimatorsData.estimators; @@ -440,6 +448,59 @@ class CopilotTokenTracker implements vscode.Disposable { return result; } + private async calculateDailyStats(): Promise { + const now = new Date(); + const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); + + // Map to store daily stats by date string (YYYY-MM-DD) + const dailyStatsMap = new Map(); + + try { + const sessionFiles = await this.getCopilotSessionFiles(); + this.log(`Processing ${sessionFiles.length} session files for daily chart stats`); + + for (const sessionFile of sessionFiles) { + try { + const fileStats = fs.statSync(sessionFile); + + // Only process files modified in the current month + if (fileStats.mtime >= monthStart) { + const tokens = await this.estimateTokensFromSessionCached(sessionFile, fileStats.mtime.getTime()); + const interactions = await this.countInteractionsInSessionCached(sessionFile, fileStats.mtime.getTime()); + + // Get the date in YYYY-MM-DD format + const fileDate = new Date(fileStats.mtime); + const dateKey = `${fileDate.getFullYear()}-${String(fileDate.getMonth() + 1).padStart(2, '0')}-${String(fileDate.getDate()).padStart(2, '0')}`; + + // Initialize or update the daily stats + if (!dailyStatsMap.has(dateKey)) { + dailyStatsMap.set(dateKey, { + date: dateKey, + tokens: 0, + sessions: 0, + interactions: 0 + }); + } + + const dailyStats = dailyStatsMap.get(dateKey)!; + dailyStats.tokens += tokens; + dailyStats.sessions += 1; + dailyStats.interactions += interactions; + } + } catch (fileError) { + this.warn(`Error processing session file ${sessionFile} for daily stats: ${fileError}`); + } + } + } catch (error) { + this.error('Error calculating daily stats:', error); + } + + // Convert map to array and sort by date + const dailyStatsArray = Array.from(dailyStatsMap.values()).sort((a, b) => a.date.localeCompare(b.date)); + + return dailyStatsArray; + } + private async countInteractionsInSession(sessionFile: string): Promise { try { const sessionContent = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); @@ -907,6 +968,9 @@ class CopilotTokenTracker implements vscode.Disposable { case 'refresh': await this.refreshDetailsPanel(); break; + case 'showChart': + await this.showChart(); + break; } }); @@ -916,6 +980,48 @@ class CopilotTokenTracker implements vscode.Disposable { }); } + public async showChart(): Promise { + // If panel already exists, just reveal it + if (this.chartPanel) { + this.chartPanel.reveal(); + return; + } + + // Get daily stats + const dailyStats = await this.calculateDailyStats(); + + // Create webview panel + this.chartPanel = vscode.window.createWebviewPanel( + 'copilotTokenChart', + 'Token Usage Over Time', + { + viewColumn: vscode.ViewColumn.Beside, + preserveFocus: true + }, + { + enableScripts: true, + retainContextWhenHidden: false + } + ); + + // Set the HTML content + this.chartPanel.webview.html = this.getChartHtml(dailyStats); + + // Handle messages from the webview + this.chartPanel.webview.onDidReceiveMessage(async (message) => { + switch (message.command) { + case 'refresh': + await this.refreshChartPanel(); + break; + } + }); + + // Handle panel disposal + this.chartPanel.onDidDispose(() => { + this.chartPanel = undefined; + }); + } + private async refreshDetailsPanel(): Promise { if (!this.detailsPanel) { return; @@ -927,6 +1033,16 @@ class CopilotTokenTracker implements vscode.Disposable { this.detailsPanel.webview.html = this.getDetailsHtml(stats); } + private async refreshChartPanel(): Promise { + if (!this.chartPanel) { + return; + } + + // Refresh the chart webview content + const dailyStats = await this.calculateDailyStats(); + this.chartPanel.webview.html = this.getChartHtml(dailyStats); + } + private getDetailsHtml(stats: DetailedStats): string { const usedModels = new Set([ ...Object.keys(stats.today.modelUsage), @@ -1201,6 +1317,10 @@ class CopilotTokenTracker implements vscode.Disposable { 🔄 Refresh Now + @@ -1211,6 +1331,11 @@ class CopilotTokenTracker implements vscode.Disposable { // Send message to extension to refresh data vscode.postMessage({ command: 'refresh' }); } + + function showChart() { + // Send message to extension to show chart + vscode.postMessage({ command: 'showChart' }); + } `; @@ -1337,6 +1462,288 @@ class CopilotTokenTracker implements vscode.Disposable { return modelNames[model] || model; } + private getChartHtml(dailyStats: DailyTokenStats[]): string { + // Prepare data for Chart.js + const labels = dailyStats.map(stat => stat.date); + const tokensData = dailyStats.map(stat => stat.tokens); + const sessionsData = dailyStats.map(stat => stat.sessions); + + return ` + + + + + Token Usage Over Time + + + + +
+
+ 📈 + Token Usage Over Time +
+ +
+
+
Total Days
+
${dailyStats.length}
+
+
+
Total Tokens
+
${dailyStats.reduce((sum, stat) => sum + stat.tokens, 0).toLocaleString()}
+
+
+
Avg Tokens/Day
+
${dailyStats.length > 0 ? Math.round(dailyStats.reduce((sum, stat) => sum + stat.tokens, 0) / dailyStats.length).toLocaleString() : 0}
+
+
+
Total Sessions
+
${dailyStats.reduce((sum, stat) => sum + stat.sessions, 0)}
+
+
+ +
+ +
+ + +
+ + + + `; + } + public dispose(): void { if (this.updateInterval) { clearInterval(this.updateInterval); @@ -1348,6 +1755,9 @@ class CopilotTokenTracker implements vscode.Disposable { if (this.detailsPanel) { this.detailsPanel.dispose(); } + if (this.chartPanel) { + this.chartPanel.dispose(); + } this.statusBarItem.dispose(); this.outputChannel.dispose(); // Clear cache on disposal @@ -1372,8 +1782,14 @@ export function activate(context: vscode.ExtensionContext) { await tokenTracker.showDetails(); }); + // Register the show chart command + const showChartCommand = vscode.commands.registerCommand('copilot-token-tracker.showChart', async () => { + tokenTracker.log('Show chart command called'); + await tokenTracker.showChart(); + }); + // Add to subscriptions for proper cleanup - context.subscriptions.push(refreshCommand, showDetailsCommand, tokenTracker); + context.subscriptions.push(refreshCommand, showDetailsCommand, showChartCommand, tokenTracker); tokenTracker.log('Extension activation complete'); } From 97fd330eed9fa1139eb7b6b9b7ed778919fa19c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:02:22 +0000 Subject: [PATCH 4/5] Address code review feedback: extract date formatting helper and optimize calculations Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- src/extension.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 6d5071e..df21d58 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -448,6 +448,10 @@ class CopilotTokenTracker implements vscode.Disposable { return result; } + private formatDateKey(date: Date): string { + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + } + private async calculateDailyStats(): Promise { const now = new Date(); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); @@ -469,8 +473,7 @@ class CopilotTokenTracker implements vscode.Disposable { const interactions = await this.countInteractionsInSessionCached(sessionFile, fileStats.mtime.getTime()); // Get the date in YYYY-MM-DD format - const fileDate = new Date(fileStats.mtime); - const dateKey = `${fileDate.getFullYear()}-${String(fileDate.getMonth() + 1).padStart(2, '0')}-${String(fileDate.getDate()).padStart(2, '0')}`; + const dateKey = this.formatDateKey(new Date(fileStats.mtime)); // Initialize or update the daily stats if (!dailyStatsMap.has(dateKey)) { @@ -1468,6 +1471,11 @@ class CopilotTokenTracker implements vscode.Disposable { const tokensData = dailyStats.map(stat => stat.tokens); const sessionsData = dailyStats.map(stat => stat.sessions); + // Pre-calculate summary statistics + const totalTokens = dailyStats.reduce((sum, stat) => sum + stat.tokens, 0); + const totalSessions = dailyStats.reduce((sum, stat) => sum + stat.sessions, 0); + const avgTokensPerDay = dailyStats.length > 0 ? Math.round(totalTokens / dailyStats.length) : 0; + return ` @@ -1586,15 +1594,15 @@ class CopilotTokenTracker implements vscode.Disposable {
Total Tokens
-
${dailyStats.reduce((sum, stat) => sum + stat.tokens, 0).toLocaleString()}
+
${totalTokens.toLocaleString()}
Avg Tokens/Day
-
${dailyStats.length > 0 ? Math.round(dailyStats.reduce((sum, stat) => sum + stat.tokens, 0) / dailyStats.length).toLocaleString() : 0}
+
${avgTokensPerDay.toLocaleString()}
Total Sessions
-
${dailyStats.reduce((sum, stat) => sum + stat.sessions, 0)}
+
${totalSessions}
@@ -1619,7 +1627,8 @@ class CopilotTokenTracker implements vscode.Disposable { vscode.postMessage({ command: 'refresh' }); } - // Chart.js configuration + // Chart.js configuration with mixed chart types (bar + line) + // Note: Using type property at dataset level for Chart.js v4 mixed charts const ctx = document.getElementById('tokenChart').getContext('2d'); const chart = new Chart(ctx, { type: 'bar', From 1217b21beca11922a4185c9b49445b538b197b25 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Sun, 4 Jan 2026 21:55:53 +0100 Subject: [PATCH 5/5] Add chart visualisation --- .vscode/tasks.json | 1 + src/extension.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 76b0e4f..0d8a598 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,6 +4,7 @@ "version": "2.0.0", "tasks": [ { + "label": "npm compile", "type": "npm", "script": "compile", "group": { diff --git a/src/extension.ts b/src/extension.ts index df21d58..c33d34a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -277,6 +277,12 @@ class CopilotTokenTracker implements vscode.Disposable { this.detailsPanel.webview.html = this.getDetailsHtml(detailedStats); } + // If the chart panel is open, update its content + if (this.chartPanel) { + const dailyStats = await this.calculateDailyStats(); + this.chartPanel.webview.html = this.getChartHtml(dailyStats); + } + this.log(`Updated stats - Today: ${detailedStats.today.tokens}, Month: ${detailedStats.month.tokens}`); } catch (error) { this.error('Error updating token stats:', error); @@ -953,7 +959,7 @@ class CopilotTokenTracker implements vscode.Disposable { 'copilotTokenDetails', 'GitHub Copilot Token Usage', { - viewColumn: vscode.ViewColumn.Beside, + viewColumn: vscode.ViewColumn.One, preserveFocus: true }, { @@ -998,7 +1004,7 @@ class CopilotTokenTracker implements vscode.Disposable { 'copilotTokenChart', 'Token Usage Over Time', { - viewColumn: vscode.ViewColumn.Beside, + viewColumn: vscode.ViewColumn.One, preserveFocus: true }, { @@ -1613,6 +1619,10 @@ class CopilotTokenTracker implements vscode.Disposable {