Skip to content

Commit 18f6ee2

Browse files
authored
Merge pull request #393 from rajbos/env-impact
Adding environmental impact view
2 parents 70e0f73 + d358d62 commit 18f6ee2

15 files changed

Lines changed: 691 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
259259
* add information where logs from the devContainer are stored by @UncleBats in https://github.com/rajbos/github-copilot-token-usage/pull/160
260260
* Persist active tab state in diagnostic view by @Copilot in https://github.com/rajbos/github-copilot-token-usage/pull/164
261261
* Add Azure Storage backend configuration panel to diagnostics by @Copilot in https://github.com/rajbos/github-copilot-token-usage/pull/163
262-
* Show last month stats next to this month by @rajbos in https://github.com/rajbos/github-copilot-token-usage/pull/166
262+
* Show Previous Month stats next to this month by @rajbos in https://github.com/rajbos/github-copilot-token-usage/pull/166
263263
* Add clickable links for empty sessions in Diagnostic Report by @Copilot in https://github.com/rajbos/github-copilot-token-usage/pull/165
264264
* Enhance usage analysis with model tracking features by @FokkoVeegens in https://github.com/rajbos/github-copilot-token-usage/pull/157
265265
* Progressive loading for diagnostics view - eliminate 10-30s UI blocking by @Copilot in https://github.com/rajbos/github-copilot-token-usage/pull/169
348 KB
Loading

esbuild.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ async function main() {
5151
logviewer: 'src/webview/logviewer/main.ts',
5252
maturity: 'src/webview/maturity/main.ts',
5353
dashboard: 'src/webview/dashboard/main.ts',
54-
'fluency-level-viewer': 'src/webview/fluency-level-viewer/main.ts',
55-
},
54+
'fluency-level-viewer': 'src/webview/fluency-level-viewer/main.ts', environmental: 'src/webview/environmental/main.ts', },
5655
bundle: true,
5756
format: 'iife',
5857
minify: production,

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@
113113
"command": "copilot-token-tracker.showDashboard",
114114
"title": "Show Team Dashboard",
115115
"category": "Copilot Token Tracker"
116+
},
117+
{
118+
"command": "copilot-token-tracker.showEnvironmental",
119+
"title": "Show Environmental Impact",
120+
"category": "Copilot Token Tracker"
116121
}
117122
],
118123
"configuration": {

src/extension.ts

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ class CopilotTokenTracker implements vscode.Disposable {
166166
private maturityPanel: vscode.WebviewPanel | undefined;
167167
private dashboardPanel: vscode.WebviewPanel | undefined;
168168
private fluencyLevelViewerPanel: vscode.WebviewPanel | undefined;
169+
private environmentalPanel: vscode.WebviewPanel | undefined;
169170
private outputChannel: vscode.OutputChannel;
170171
private lastDetailedStats: DetailedStats | undefined;
171172
private lastDailyStats: DailyTokenStats[] | undefined;
@@ -672,6 +673,25 @@ class CopilotTokenTracker implements vscode.Disposable {
672673
this.maturityPanel.webview.html = this.getMaturityHtml(this.maturityPanel.webview, maturityData);
673674
}
674675

676+
// If the environmental panel is open, update its content
677+
if (this.environmentalPanel) {
678+
if (silent) {
679+
void this.environmentalPanel.webview.postMessage({
680+
command: 'updateStats',
681+
data: {
682+
today: detailedStats.today,
683+
month: detailedStats.month,
684+
lastMonth: detailedStats.lastMonth,
685+
last30Days: detailedStats.last30Days,
686+
lastUpdated: detailedStats.lastUpdated.toISOString(),
687+
backendConfigured: this.isBackendConfigured(),
688+
},
689+
});
690+
} else {
691+
this.environmentalPanel.webview.html = this.getEnvironmentalHtml(this.environmentalPanel.webview, detailedStats);
692+
}
693+
}
694+
675695
this.log(`Updated stats - Today: ${detailedStats.today.tokens}, Last 30 Days: ${detailedStats.last30Days.tokens}`);
676696
// Store the stats for reuse without recalculation
677697
this.lastDetailedStats = detailedStats;
@@ -738,7 +758,7 @@ class CopilotTokenTracker implements vscode.Disposable {
738758
const now = new Date();
739759
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
740760
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
741-
// Calculate last month boundaries
761+
// Calculate Previous Month boundaries
742762
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999); // Last day of previous month
743763
const lastMonthStart = new Date(lastMonthEnd.getFullYear(), lastMonthEnd.getMonth(), 1);
744764
// Calculate last 30 days boundary
@@ -930,22 +950,22 @@ class CopilotTokenTracker implements vscode.Disposable {
930950
}
931951
}
932952
else if (lastActivity >= lastMonthStart && lastActivity <= lastMonthEnd) {
933-
// Session is from last month - only track lastMonth stats
953+
// Session is from Previous Month - only track lastMonth stats
934954
lastMonthStats.tokens += tokens;
935955
lastMonthStats.estimatedTokens += estimatedTokens;
936956
lastMonthStats.actualTokens += actualTokens;
937957
lastMonthStats.thinkingTokens += (sessionData.thinkingTokens || 0);
938958
lastMonthStats.sessions += 1;
939959
lastMonthStats.interactions += interactions;
940960

941-
// Add editor usage to last month stats
961+
// Add editor usage to Previous Month stats
942962
if (!lastMonthStats.editorUsage[editorType]) {
943963
lastMonthStats.editorUsage[editorType] = { tokens: 0, sessions: 0 };
944964
}
945965
lastMonthStats.editorUsage[editorType].tokens += tokens;
946966
lastMonthStats.editorUsage[editorType].sessions += 1;
947967

948-
// Add model usage to last month stats
968+
// Add model usage to Previous Month stats
949969
for (const [model, usage] of Object.entries(modelUsage)) {
950970
if (!lastMonthStats.modelUsage[model]) {
951971
lastMonthStats.modelUsage[model] = { inputTokens: 0, outputTokens: 0 };
@@ -963,7 +983,7 @@ class CopilotTokenTracker implements vscode.Disposable {
963983
}
964984
}
965985

966-
this.log(`✅ Analysis complete: Today ${todayStats.sessions} sessions, Month ${monthStats.sessions} sessions, Last 30 Days ${last30DaysStats.sessions} sessions, Last Month ${lastMonthStats.sessions} sessions`);
986+
this.log(`✅ Analysis complete: Today ${todayStats.sessions} sessions, Month ${monthStats.sessions} sessions, Last 30 Days ${last30DaysStats.sessions} sessions, Previous Month ${lastMonthStats.sessions} sessions`);
967987
if (skippedFiles > 0) {
968988
this.log(`⏭️ Skipped ${skippedFiles} session file(s) (empty or no activity in recent months)`);
969989
}
@@ -3051,6 +3071,9 @@ class CopilotTokenTracker implements vscode.Disposable {
30513071
case 'showDashboard':
30523072
await this.showDashboard();
30533073
break;
3074+
case 'showEnvironmental':
3075+
await this.showEnvironmental();
3076+
break;
30543077
case 'saveSortSettings':
30553078
await this.context.globalState.update('details.sortSettings', message.settings);
30563079
break;
@@ -3064,6 +3087,108 @@ class CopilotTokenTracker implements vscode.Disposable {
30643087
});
30653088
}
30663089

3090+
public async showEnvironmental(): Promise<void> {
3091+
this.log('🌿 Opening Environmental Impact view');
3092+
3093+
if (this.environmentalPanel) {
3094+
this.environmentalPanel.reveal();
3095+
this.log('🌿 Environmental Impact view revealed (already exists)');
3096+
return;
3097+
}
3098+
3099+
let stats = this.lastDetailedStats;
3100+
if (!stats) {
3101+
stats = await this.updateTokenStats();
3102+
if (!stats) {
3103+
return;
3104+
}
3105+
}
3106+
3107+
this.environmentalPanel = vscode.window.createWebviewPanel(
3108+
'copilotEnvironmental',
3109+
'Environmental Impact',
3110+
{ viewColumn: vscode.ViewColumn.One, preserveFocus: true },
3111+
{
3112+
enableScripts: true,
3113+
retainContextWhenHidden: false,
3114+
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview')]
3115+
}
3116+
);
3117+
3118+
this.environmentalPanel.webview.html = this.getEnvironmentalHtml(this.environmentalPanel.webview, stats);
3119+
3120+
this.environmentalPanel.webview.onDidReceiveMessage(async (message) => {
3121+
switch (message.command) {
3122+
case 'refresh': {
3123+
const refreshed = await this.updateTokenStats();
3124+
if (refreshed && this.environmentalPanel) {
3125+
this.environmentalPanel.webview.html = this.getEnvironmentalHtml(this.environmentalPanel.webview, refreshed);
3126+
}
3127+
break;
3128+
}
3129+
case 'showDetails':
3130+
await this.showDetails();
3131+
break;
3132+
case 'showChart':
3133+
await this.showChart();
3134+
break;
3135+
case 'showUsageAnalysis':
3136+
await this.showUsageAnalysis();
3137+
break;
3138+
case 'showDiagnostics':
3139+
await this.showDiagnosticReport();
3140+
break;
3141+
case 'showMaturity':
3142+
await this.showMaturity();
3143+
break;
3144+
case 'showDashboard':
3145+
await this.showDashboard();
3146+
break;
3147+
}
3148+
});
3149+
3150+
this.environmentalPanel.onDidDispose(() => {
3151+
this.log('🌿 Environmental Impact view closed');
3152+
this.environmentalPanel = undefined;
3153+
});
3154+
}
3155+
3156+
private getEnvironmentalHtml(webview: vscode.Webview, stats: DetailedStats): string {
3157+
const nonce = this.getNonce();
3158+
const scriptUri = webview.asWebviewUri(
3159+
vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview', 'environmental.js')
3160+
);
3161+
3162+
const csp = [
3163+
`default-src 'none'`,
3164+
`img-src ${webview.cspSource} https: data:`,
3165+
`style-src 'unsafe-inline' ${webview.cspSource}`,
3166+
`font-src ${webview.cspSource} https: data:`,
3167+
`script-src 'nonce-${nonce}'`,
3168+
].join('; ');
3169+
3170+
const dataWithBackend = {
3171+
...stats,
3172+
backendConfigured: this.isBackendConfigured(),
3173+
};
3174+
const initialData = JSON.stringify(dataWithBackend).replace(/</g, '\\u003c');
3175+
3176+
return `<!DOCTYPE html>
3177+
<html lang="en">
3178+
<head>
3179+
<meta charset="UTF-8" />
3180+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
3181+
<meta http-equiv="Content-Security-Policy" content="${csp}" />
3182+
<title>Environmental Impact</title>
3183+
</head>
3184+
<body>
3185+
<div id="root"></div>
3186+
<script nonce="${nonce}">window.__INITIAL_ENVIRONMENTAL__ = ${initialData};</script>
3187+
<script nonce="${nonce}" src="${scriptUri}"></script>
3188+
</body>
3189+
</html>`;
3190+
}
3191+
30673192
public async showChart(): Promise<void> {
30683193
this.log('📈 Opening Chart view');
30693194

@@ -3118,6 +3243,9 @@ class CopilotTokenTracker implements vscode.Disposable {
31183243
case 'showDashboard':
31193244
await this.showDashboard();
31203245
break;
3246+
case 'showEnvironmental':
3247+
await this.showEnvironmental();
3248+
break;
31213249
}
31223250
});
31233251

@@ -3182,6 +3310,9 @@ class CopilotTokenTracker implements vscode.Disposable {
31823310
case 'showDashboard':
31833311
await this.showDashboard();
31843312
break;
3313+
case 'showEnvironmental':
3314+
await this.showEnvironmental();
3315+
break;
31853316
case 'analyseRepository':
31863317
await this.handleAnalyseRepository(message.workspacePath);
31873318
break;
@@ -3891,6 +4022,9 @@ Return ONLY the JSON object, no markdown formatting, no explanations.`;
38914022
case 'showDashboard':
38924023
await this.showDashboard();
38934024
break;
4025+
case 'showEnvironmental':
4026+
await this.showEnvironmental();
4027+
break;
38944028
case 'shareToLinkedIn':
38954029
await this.shareToSocialMedia('linkedin');
38964030
break;
@@ -4591,6 +4725,9 @@ ${hashtag}`;
45914725
case "showMaturity":
45924726
await this.showMaturity();
45934727
break;
4728+
case "showEnvironmental":
4729+
await this.showEnvironmental();
4730+
break;
45944731
case "deleteUserDataset":
45954732
await this.handleDeleteUserDataset(message.userId, message.datasetId);
45964733
break;
@@ -5681,6 +5818,9 @@ ${hashtag}`;
56815818
case "showDashboard":
56825819
await this.showDashboard();
56835820
break;
5821+
case "showEnvironmental":
5822+
await this.showEnvironmental();
5823+
break;
56845824
case "resetDebugCounters":
56855825
await this.context.globalState.update('extension.openCount', 0);
56865826
await this.context.globalState.update('extension.unknownMcpOpenCount', 0);
@@ -6529,6 +6669,14 @@ export function activate(context: vscode.ExtensionContext) {
65296669
},
65306670
);
65316671

6672+
const showEnvironmentalCommand = vscode.commands.registerCommand(
6673+
"copilot-token-tracker.showEnvironmental",
6674+
async () => {
6675+
tokenTracker.log("Show environmental impact command called");
6676+
await tokenTracker.showEnvironmental();
6677+
},
6678+
);
6679+
65326680
// Register the show fluency level viewer command (debug-only)
65336681
const showFluencyLevelViewerCommand = vscode.commands.registerCommand(
65346682
"copilot-token-tracker.showFluencyLevelViewer",
@@ -6565,6 +6713,7 @@ export function activate(context: vscode.ExtensionContext) {
65656713
showMaturityCommand,
65666714
showFluencyLevelViewerCommand,
65676715
showDashboardCommand,
6716+
showEnvironmentalCommand,
65686717
generateDiagnosticReportCommand,
65696718
clearCacheCommand,
65706719
tokenTracker,

src/webview/chart/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ function renderLayout(data: InitialChartData): void {
8585
createButton(BUTTONS['btn-refresh']),
8686
createButton(BUTTONS['btn-details']),
8787
createButton(BUTTONS['btn-usage']),
88+
createButton(BUTTONS['btn-environmental']),
8889
createButton(BUTTONS['btn-diagnostics']),
8990
createButton(BUTTONS['btn-maturity'])
9091
);
@@ -178,6 +179,9 @@ function wireInteractions(data: InitialChartData): void {
178179
const dashboard = document.getElementById('btn-dashboard');
179180
dashboard?.addEventListener('click', () => vscode.postMessage({ command: 'showDashboard' }));
180181

182+
const environmental = document.getElementById('btn-environmental');
183+
environmental?.addEventListener('click', () => vscode.postMessage({ command: 'showEnvironmental' }));
184+
181185
const viewButtons = [
182186
{ id: 'view-total', view: 'total' as const },
183187
{ id: 'view-model', view: 'model' as const },

src/webview/dashboard/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ function renderShell(root: HTMLElement, stats: DashboardStats): void {
160160
createButton(BUTTONS["btn-details"]),
161161
createButton(BUTTONS["btn-chart"]),
162162
createButton(BUTTONS["btn-usage"]),
163+
createButton(BUTTONS["btn-environmental"]),
163164
createButton(BUTTONS["btn-diagnostics"]),
164165
createButton(BUTTONS["btn-maturity"]),
165166
);
@@ -569,6 +570,9 @@ function wireButtons(): void {
569570
document.getElementById("btn-maturity")?.addEventListener("click", () => {
570571
vscode.postMessage({ command: "showMaturity" });
571572
});
573+
document.getElementById("btn-environmental")?.addEventListener("click", () => {
574+
vscode.postMessage({ command: "showEnvironmental" });
575+
});
572576

573577
// Note: No dashboard button handler - users are already on the dashboard
574578
}

0 commit comments

Comments
 (0)