Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
* add information where logs from the devContainer are stored by @UncleBats in https://github.com/rajbos/github-copilot-token-usage/pull/160
* Persist active tab state in diagnostic view by @Copilot in https://github.com/rajbos/github-copilot-token-usage/pull/164
* Add Azure Storage backend configuration panel to diagnostics by @Copilot in https://github.com/rajbos/github-copilot-token-usage/pull/163
* Show last month stats next to this month by @rajbos in https://github.com/rajbos/github-copilot-token-usage/pull/166
* Show Previous Month stats next to this month by @rajbos in https://github.com/rajbos/github-copilot-token-usage/pull/166
* Add clickable links for empty sessions in Diagnostic Report by @Copilot in https://github.com/rajbos/github-copilot-token-usage/pull/165
* Enhance usage analysis with model tracking features by @FokkoVeegens in https://github.com/rajbos/github-copilot-token-usage/pull/157
* Progressive loading for diagnostics view - eliminate 10-30s UI blocking by @Copilot in https://github.com/rajbos/github-copilot-token-usage/pull/169
Expand Down
Binary file added docs/images/EnvironmentalImpact.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ async function main() {
logviewer: 'src/webview/logviewer/main.ts',
maturity: 'src/webview/maturity/main.ts',
dashboard: 'src/webview/dashboard/main.ts',
'fluency-level-viewer': 'src/webview/fluency-level-viewer/main.ts',
},
'fluency-level-viewer': 'src/webview/fluency-level-viewer/main.ts', environmental: 'src/webview/environmental/main.ts', },
bundle: true,
format: 'iife',
minify: production,
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@
"command": "copilot-token-tracker.showDashboard",
"title": "Show Team Dashboard",
"category": "Copilot Token Tracker"
},
{
"command": "copilot-token-tracker.showEnvironmental",
"title": "Show Environmental Impact",
"category": "Copilot Token Tracker"
}
],
"configuration": {
Expand Down
159 changes: 154 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ class CopilotTokenTracker implements vscode.Disposable {
private maturityPanel: vscode.WebviewPanel | undefined;
private dashboardPanel: vscode.WebviewPanel | undefined;
private fluencyLevelViewerPanel: vscode.WebviewPanel | undefined;
private environmentalPanel: vscode.WebviewPanel | undefined;
private outputChannel: vscode.OutputChannel;
private lastDetailedStats: DetailedStats | undefined;
private lastDailyStats: DailyTokenStats[] | undefined;
Expand Down Expand Up @@ -672,6 +673,25 @@ class CopilotTokenTracker implements vscode.Disposable {
this.maturityPanel.webview.html = this.getMaturityHtml(this.maturityPanel.webview, maturityData);
}

// If the environmental panel is open, update its content
if (this.environmentalPanel) {
if (silent) {
void this.environmentalPanel.webview.postMessage({
command: 'updateStats',
data: {
today: detailedStats.today,
month: detailedStats.month,
lastMonth: detailedStats.lastMonth,
last30Days: detailedStats.last30Days,
lastUpdated: detailedStats.lastUpdated.toISOString(),
backendConfigured: this.isBackendConfigured(),
},
});
} else {
this.environmentalPanel.webview.html = this.getEnvironmentalHtml(this.environmentalPanel.webview, detailedStats);
}
}

this.log(`Updated stats - Today: ${detailedStats.today.tokens}, Last 30 Days: ${detailedStats.last30Days.tokens}`);
// Store the stats for reuse without recalculation
this.lastDetailedStats = detailedStats;
Expand Down Expand Up @@ -738,7 +758,7 @@ class CopilotTokenTracker implements vscode.Disposable {
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
// Calculate last month boundaries
// Calculate Previous 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
Expand Down Expand Up @@ -930,22 +950,22 @@ class CopilotTokenTracker implements vscode.Disposable {
}
}
else if (lastActivity >= lastMonthStart && lastActivity <= lastMonthEnd) {
// Session is from last month - only track lastMonth stats
// Session is from Previous Month - only track lastMonth stats
lastMonthStats.tokens += tokens;
lastMonthStats.estimatedTokens += estimatedTokens;
lastMonthStats.actualTokens += actualTokens;
lastMonthStats.thinkingTokens += (sessionData.thinkingTokens || 0);
lastMonthStats.sessions += 1;
lastMonthStats.interactions += interactions;

// Add editor usage to last month stats
// Add editor usage to Previous Month stats
if (!lastMonthStats.editorUsage[editorType]) {
lastMonthStats.editorUsage[editorType] = { tokens: 0, sessions: 0 };
}
lastMonthStats.editorUsage[editorType].tokens += tokens;
lastMonthStats.editorUsage[editorType].sessions += 1;

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

this.log(`✅ Analysis complete: Today ${todayStats.sessions} sessions, Month ${monthStats.sessions} sessions, Last 30 Days ${last30DaysStats.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, Previous Month ${lastMonthStats.sessions} sessions`);
if (skippedFiles > 0) {
this.log(`⏭️ Skipped ${skippedFiles} session file(s) (empty or no activity in recent months)`);
}
Expand Down Expand Up @@ -3051,6 +3071,9 @@ class CopilotTokenTracker implements vscode.Disposable {
case 'showDashboard':
await this.showDashboard();
break;
case 'showEnvironmental':
await this.showEnvironmental();
break;
case 'saveSortSettings':
await this.context.globalState.update('details.sortSettings', message.settings);
break;
Expand All @@ -3064,6 +3087,108 @@ class CopilotTokenTracker implements vscode.Disposable {
});
}

public async showEnvironmental(): Promise<void> {
this.log('🌿 Opening Environmental Impact view');

if (this.environmentalPanel) {
this.environmentalPanel.reveal();
this.log('🌿 Environmental Impact view revealed (already exists)');
return;
}

let stats = this.lastDetailedStats;
if (!stats) {
stats = await this.updateTokenStats();
if (!stats) {
return;
}
}

this.environmentalPanel = vscode.window.createWebviewPanel(
'copilotEnvironmental',
'Environmental Impact',
{ viewColumn: vscode.ViewColumn.One, preserveFocus: true },
{
enableScripts: true,
retainContextWhenHidden: false,
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview')]
}
);

this.environmentalPanel.webview.html = this.getEnvironmentalHtml(this.environmentalPanel.webview, stats);

this.environmentalPanel.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'refresh': {
const refreshed = await this.updateTokenStats();
if (refreshed && this.environmentalPanel) {
this.environmentalPanel.webview.html = this.getEnvironmentalHtml(this.environmentalPanel.webview, refreshed);
}
break;
}
case 'showDetails':
await this.showDetails();
break;
case 'showChart':
await this.showChart();
break;
case 'showUsageAnalysis':
await this.showUsageAnalysis();
break;
case 'showDiagnostics':
await this.showDiagnosticReport();
break;
case 'showMaturity':
await this.showMaturity();
break;
case 'showDashboard':
await this.showDashboard();
break;
}
});

this.environmentalPanel.onDidDispose(() => {
this.log('🌿 Environmental Impact view closed');
this.environmentalPanel = undefined;
});
}

private getEnvironmentalHtml(webview: vscode.Webview, stats: DetailedStats): string {
const nonce = this.getNonce();
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview', 'environmental.js')
);

const csp = [
`default-src 'none'`,
`img-src ${webview.cspSource} https: data:`,
`style-src 'unsafe-inline' ${webview.cspSource}`,
`font-src ${webview.cspSource} https: data:`,
`script-src 'nonce-${nonce}'`,
].join('; ');

const dataWithBackend = {
...stats,
backendConfigured: this.isBackendConfigured(),
};
const initialData = JSON.stringify(dataWithBackend).replace(/</g, '\\u003c');

return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="${csp}" />
<title>Environmental Impact</title>
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}">window.__INITIAL_ENVIRONMENTAL__ = ${initialData};</script>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
}

public async showChart(): Promise<void> {
this.log('📈 Opening Chart view');

Expand Down Expand Up @@ -3118,6 +3243,9 @@ class CopilotTokenTracker implements vscode.Disposable {
case 'showDashboard':
await this.showDashboard();
break;
case 'showEnvironmental':
await this.showEnvironmental();
break;
}
});

Expand Down Expand Up @@ -3182,6 +3310,9 @@ class CopilotTokenTracker implements vscode.Disposable {
case 'showDashboard':
await this.showDashboard();
break;
case 'showEnvironmental':
await this.showEnvironmental();
break;
case 'analyseRepository':
await this.handleAnalyseRepository(message.workspacePath);
break;
Expand Down Expand Up @@ -3891,6 +4022,9 @@ Return ONLY the JSON object, no markdown formatting, no explanations.`;
case 'showDashboard':
await this.showDashboard();
break;
case 'showEnvironmental':
await this.showEnvironmental();
break;
case 'shareToLinkedIn':
await this.shareToSocialMedia('linkedin');
break;
Expand Down Expand Up @@ -4591,6 +4725,9 @@ ${hashtag}`;
case "showMaturity":
await this.showMaturity();
break;
case "showEnvironmental":
await this.showEnvironmental();
break;
case "deleteUserDataset":
await this.handleDeleteUserDataset(message.userId, message.datasetId);
break;
Expand Down Expand Up @@ -5681,6 +5818,9 @@ ${hashtag}`;
case "showDashboard":
await this.showDashboard();
break;
case "showEnvironmental":
await this.showEnvironmental();
break;
case "resetDebugCounters":
await this.context.globalState.update('extension.openCount', 0);
await this.context.globalState.update('extension.unknownMcpOpenCount', 0);
Expand Down Expand Up @@ -6529,6 +6669,14 @@ export function activate(context: vscode.ExtensionContext) {
},
);

const showEnvironmentalCommand = vscode.commands.registerCommand(
"copilot-token-tracker.showEnvironmental",
async () => {
tokenTracker.log("Show environmental impact command called");
await tokenTracker.showEnvironmental();
},
);

// Register the show fluency level viewer command (debug-only)
const showFluencyLevelViewerCommand = vscode.commands.registerCommand(
"copilot-token-tracker.showFluencyLevelViewer",
Expand Down Expand Up @@ -6565,6 +6713,7 @@ export function activate(context: vscode.ExtensionContext) {
showMaturityCommand,
showFluencyLevelViewerCommand,
showDashboardCommand,
showEnvironmentalCommand,
generateDiagnosticReportCommand,
clearCacheCommand,
tokenTracker,
Expand Down
4 changes: 4 additions & 0 deletions src/webview/chart/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function renderLayout(data: InitialChartData): void {
createButton(BUTTONS['btn-refresh']),
createButton(BUTTONS['btn-details']),
createButton(BUTTONS['btn-usage']),
createButton(BUTTONS['btn-environmental']),
createButton(BUTTONS['btn-diagnostics']),
createButton(BUTTONS['btn-maturity'])
);
Expand Down Expand Up @@ -178,6 +179,9 @@ function wireInteractions(data: InitialChartData): void {
const dashboard = document.getElementById('btn-dashboard');
dashboard?.addEventListener('click', () => vscode.postMessage({ command: 'showDashboard' }));

const environmental = document.getElementById('btn-environmental');
environmental?.addEventListener('click', () => vscode.postMessage({ command: 'showEnvironmental' }));

const viewButtons = [
{ id: 'view-total', view: 'total' as const },
{ id: 'view-model', view: 'model' as const },
Expand Down
4 changes: 4 additions & 0 deletions src/webview/dashboard/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ function renderShell(root: HTMLElement, stats: DashboardStats): void {
createButton(BUTTONS["btn-details"]),
createButton(BUTTONS["btn-chart"]),
createButton(BUTTONS["btn-usage"]),
createButton(BUTTONS["btn-environmental"]),
createButton(BUTTONS["btn-diagnostics"]),
createButton(BUTTONS["btn-maturity"]),
);
Expand Down Expand Up @@ -569,6 +570,9 @@ function wireButtons(): void {
document.getElementById("btn-maturity")?.addEventListener("click", () => {
vscode.postMessage({ command: "showMaturity" });
});
document.getElementById("btn-environmental")?.addEventListener("click", () => {
vscode.postMessage({ command: "showEnvironmental" });
});

// Note: No dashboard button handler - users are already on the dashboard
}
Expand Down
Loading
Loading