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/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" 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..c33d34a 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; @@ -269,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); @@ -440,6 +454,62 @@ 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); + + // 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 dateKey = this.formatDateKey(new Date(fileStats.mtime)); + + // 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')); @@ -889,7 +959,7 @@ class CopilotTokenTracker implements vscode.Disposable { 'copilotTokenDetails', 'GitHub Copilot Token Usage', { - viewColumn: vscode.ViewColumn.Beside, + viewColumn: vscode.ViewColumn.One, preserveFocus: true }, { @@ -907,6 +977,9 @@ class CopilotTokenTracker implements vscode.Disposable { case 'refresh': await this.refreshDetailsPanel(); break; + case 'showChart': + await this.showChart(); + break; } }); @@ -916,6 +989,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.One, + 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 +1042,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 +1326,10 @@ class CopilotTokenTracker implements vscode.Disposable { 🔄 Refresh Now + @@ -1211,6 +1340,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 +1471,298 @@ 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); + + // 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 ` + + + + + Token Usage Over Time + + + + +
+
+ 📈 + Token Usage Over Time +
+ +
+
+
Total Days
+
${dailyStats.length}
+
+
+
Total Tokens
+
${totalTokens.toLocaleString()}
+
+
+
Avg Tokens/Day
+
${avgTokensPerDay.toLocaleString()}
+
+
+
Total Sessions
+
${totalSessions}
+
+
+ +
+ +
+ + +
+ + + + `; + } + public dispose(): void { if (this.updateInterval) { clearInterval(this.updateInterval); @@ -1348,6 +1774,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 +1801,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'); }