Skip to content

Commit 6d4daf7

Browse files
authored
Merge pull request #134 from rajbos/releasing
Refactoring views
2 parents 91cc209 + 64fd231 commit 6d4daf7

12 files changed

Lines changed: 310 additions & 592 deletions

File tree

.github/skills/copilot-log-analysis/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ Recursively scans directories for `.json` and `.jsonl` session files.
124124
- `kind: 1` → input tokens (from `request.message.parts[].text`)
125125
- `kind: 2` → output tokens (from `response[].value`)
126126

127-
**Model Detection Logic**: `getModelFromRequest()` (lines 1123-1145)
127+
**Model Detection Logic**: `getModelFromRequest()`
128128
- Primary: `request.result.metadata.modelId`
129129
- Fallback: Parse `request.result.details` string for model names
130130
- Detected patterns (defined in code lines 1129-1143):
@@ -133,7 +133,7 @@ Recursively scans directories for `.json` and `.jsonl` session files.
133133
- Google: Gemini 2.5 Pro, Gemini 3 Pro (Preview), Gemini 3 Pro
134134
- Default fallback: gpt-4
135135

136-
**Note**: The display name mapping in `getModelDisplayName()` (lines 1778-1811) includes additional model variants (GPT-5 family, Claude Haiku, Claude Opus, Gemini 3 Flash, Grok, Raptor) that may appear if specified via `metadata.modelId` but are not pattern-matched from `result.details`.
136+
**Note**: The display name mapping in `getModelDisplayName()` includes additional model variants (GPT-5 family, Claude Haiku, Claude Opus, Gemini 3 Flash, Grok, Raptor) that may appear if specified via `metadata.modelId` but are not pattern-matched from `result.details`.
137137

138138
### 4. Editor Type Detection: `getEditorTypeFromPath()`
139139
**Location**: `src/extension.ts` (lines 111-143)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "copilot-token-tracker",
33
"displayName": "Copilot Token Tracker",
44
"description": "Shows daily and monthly (estimated) GitHub Copilot token usage stats in VS Code status bar",
5-
"version": "0.0.8",
5+
"version": "0.0.9",
66
"publisher": "RobBos",
77
"engines": {
88
"vscode": "^1.108.0"

src/extension.ts

Lines changed: 44 additions & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as os from 'os';
55
import tokenEstimatorsData from './tokenEstimators.json';
66
import modelPricingData from './modelPricing.json';
77
import * as packageJson from '../package.json';
8+
import {getModelDisplayName} from './webview/shared/modelUtils';
89

910
interface TokenUsageStats {
1011
todayTokens: number;
@@ -789,7 +790,43 @@ class CopilotTokenTracker implements vscode.Disposable {
789790
}
790791

791792
// Convert map to array and sort by date
792-
const dailyStatsArray = Array.from(dailyStatsMap.values()).sort((a, b) => a.date.localeCompare(b.date));
793+
let dailyStatsArray = Array.from(dailyStatsMap.values()).sort((a, b) => a.date.localeCompare(b.date));
794+
795+
// Fill in missing dates between the first date and today
796+
if (dailyStatsArray.length > 0) {
797+
const firstDate = new Date(dailyStatsArray[0].date);
798+
const today = new Date();
799+
800+
// Create a set of existing dates for quick lookup
801+
const existingDates = new Set(dailyStatsArray.map(s => s.date));
802+
803+
// Generate all dates from first date to today
804+
const allDates: string[] = [];
805+
const currentDate = new Date(firstDate);
806+
807+
while (currentDate <= today) {
808+
const dateKey = this.formatDateKey(currentDate);
809+
allDates.push(dateKey);
810+
currentDate.setDate(currentDate.getDate() + 1);
811+
}
812+
813+
// Add missing dates with zero values
814+
for (const dateKey of allDates) {
815+
if (!existingDates.has(dateKey)) {
816+
dailyStatsMap.set(dateKey, {
817+
date: dateKey,
818+
tokens: 0,
819+
sessions: 0,
820+
interactions: 0,
821+
modelUsage: {},
822+
editorUsage: {}
823+
});
824+
}
825+
}
826+
827+
// Re-convert map to array and sort by date
828+
dailyStatsArray = Array.from(dailyStatsMap.values()).sort((a, b) => a.date.localeCompare(b.date));
829+
}
793830

794831
return dailyStatsArray;
795832
}
@@ -1698,13 +1735,12 @@ class CopilotTokenTracker implements vscode.Disposable {
16981735
}
16991736
}
17001737

1701-
// Handle Copilot CLI format
1738+
// Handle Copilot CLI format (type: 'user.message')
17021739
if (event.type === 'user.message' && event.data?.content) {
17031740
turnNumber++;
17041741
const contextRefs = this.createEmptyContextRefs();
17051742
const userMessage = event.data.content;
17061743
this.analyzeContextReferences(userMessage, contextRefs);
1707-
17081744
const turn: ChatTurn = {
17091745
turnNumber,
17101746
timestamp: event.timestamp ? new Date(event.timestamp).toISOString() : null,
@@ -2308,7 +2344,7 @@ class CopilotTokenTracker implements vscode.Disposable {
23082344
{
23092345
enableScripts: true,
23102346
retainContextWhenHidden: false,
2311-
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview')]
2347+
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'dist')]
23122348
}
23132349
);
23142350

@@ -2700,7 +2736,9 @@ class CopilotTokenTracker implements vscode.Disposable {
27002736

27012737
private getDetailsHtml(webview: vscode.Webview, stats: DetailedStats): string {
27022738
const nonce = this.getNonce();
2703-
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview', 'details.js'));
2739+
const scriptUri = webview.asWebviewUri(
2740+
vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview', 'details.js')
2741+
);
27042742

27052743
const csp = [
27062744
`default-src 'none'`,
@@ -2728,233 +2766,6 @@ class CopilotTokenTracker implements vscode.Disposable {
27282766
</html>`;
27292767
}
27302768

2731-
private getModelUsageHtml(stats: DetailedStats): string {
2732-
// Get all unique models from both periods
2733-
const allModels = new Set([
2734-
...Object.keys(stats.today.modelUsage),
2735-
...Object.keys(stats.month.modelUsage)
2736-
]);
2737-
2738-
if (allModels.size === 0) {
2739-
return '';
2740-
}
2741-
2742-
const now = new Date();
2743-
const currentDayOfMonth = now.getDate();
2744-
const daysInYear = (now.getFullYear() % 4 === 0 && now.getFullYear() % 100 !== 0) || now.getFullYear() % 400 === 0 ? 366 : 365;
2745-
2746-
const calculateProjection = (monthlyValue: number) => {
2747-
if (currentDayOfMonth === 0) {
2748-
return 0;
2749-
}
2750-
const dailyAverage = monthlyValue / currentDayOfMonth;
2751-
return dailyAverage * daysInYear;
2752-
};
2753-
2754-
const modelRows = Array.from(allModels).map(model => {
2755-
const ratio = this.tokenEstimators[model] || 0.25;
2756-
const charsPerToken = (1 / ratio).toFixed(1);
2757-
2758-
const todayUsage = stats.today.modelUsage[model] || { inputTokens: 0, outputTokens: 0 };
2759-
const monthUsage = stats.month.modelUsage[model] || { inputTokens: 0, outputTokens: 0 };
2760-
2761-
const todayTotal = todayUsage.inputTokens + todayUsage.outputTokens;
2762-
const monthTotal = monthUsage.inputTokens + monthUsage.outputTokens;
2763-
const projectedTokens = calculateProjection(monthTotal);
2764-
2765-
const todayInputPercent = todayTotal > 0 ? ((todayUsage.inputTokens / todayTotal) * 100).toFixed(0) : 0;
2766-
const todayOutputPercent = todayTotal > 0 ? ((todayUsage.outputTokens / todayTotal) * 100).toFixed(0) : 0;
2767-
const monthInputPercent = monthTotal > 0 ? ((monthUsage.inputTokens / monthTotal) * 100).toFixed(0) : 0;
2768-
const monthOutputPercent = monthTotal > 0 ? ((monthUsage.outputTokens / monthTotal) * 100).toFixed(0) : 0;
2769-
2770-
return `
2771-
<tr>
2772-
<td class="metric-label">
2773-
${this.getModelDisplayName(model)}
2774-
<span style="font-size: 11px; color: #a0a0a0; font-weight: normal;">(~${charsPerToken} chars/tk)</span>
2775-
</td>
2776-
<td class="today-value">
2777-
${todayTotal.toLocaleString()}
2778-
<div style="font-size: 10px; color: #999; font-weight: normal; margin-top: 2px;">↑${todayInputPercent}% ↓${todayOutputPercent}%</div>
2779-
</td>
2780-
<td class="month-value">
2781-
${monthTotal.toLocaleString()}
2782-
<div style="font-size: 10px; color: #999; font-weight: normal; margin-top: 2px;">↑${monthInputPercent}% ↓${monthOutputPercent}%</div>
2783-
</td>
2784-
<td class="month-value">${Math.round(projectedTokens).toLocaleString()}</td>
2785-
</tr>
2786-
`;
2787-
}).join('');
2788-
2789-
return `
2790-
<div style="margin-top: 16px;">
2791-
<h3 style="color: #ffffff; font-size: 14px; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;">
2792-
<span>🎯</span>
2793-
<span>Model Usage (Tokens)</span>
2794-
</h3>
2795-
<table class="stats-table">
2796-
<colgroup>
2797-
<col class="metric-col">
2798-
<col class="value-col">
2799-
<col class="value-col">
2800-
<col class="value-col">
2801-
</colgroup>
2802-
<thead>
2803-
<tr>
2804-
<th>Model</th>
2805-
<th>
2806-
<div class="period-header">
2807-
<span>📅</span>
2808-
<span>Today</span>
2809-
</div>
2810-
</th>
2811-
<th>
2812-
<div class="period-header">
2813-
<span>📊</span>
2814-
<span>This Month</span>
2815-
</div>
2816-
</th>
2817-
<th>
2818-
<div class="period-header">
2819-
<span>🌍</span>
2820-
<span>Projected Year</span>
2821-
</div>
2822-
</th>
2823-
</tr>
2824-
</thead>
2825-
<tbody>
2826-
${modelRows}
2827-
</tbody>
2828-
</table>
2829-
</div>
2830-
`;
2831-
}
2832-
2833-
private getEditorUsageHtml(stats: DetailedStats): string {
2834-
// Get all unique editors from both periods
2835-
const allEditors = new Set([
2836-
...Object.keys(stats.today.editorUsage),
2837-
...Object.keys(stats.month.editorUsage)
2838-
]);
2839-
2840-
if (allEditors.size === 0) {
2841-
return '';
2842-
}
2843-
2844-
// Calculate totals for percentages
2845-
const todayTotal = Object.values(stats.today.editorUsage).reduce((sum, e) => sum + e.tokens, 0);
2846-
const monthTotal = Object.values(stats.month.editorUsage).reduce((sum, e) => sum + e.tokens, 0);
2847-
2848-
const editorRows = Array.from(allEditors).sort().map(editor => {
2849-
const todayUsage = stats.today.editorUsage[editor] || { tokens: 0, sessions: 0 };
2850-
const monthUsage = stats.month.editorUsage[editor] || { tokens: 0, sessions: 0 };
2851-
2852-
const todayPercent = todayTotal > 0 ? ((todayUsage.tokens / todayTotal) * 100).toFixed(1) : '0.0';
2853-
const monthPercent = monthTotal > 0 ? ((monthUsage.tokens / monthTotal) * 100).toFixed(1) : '0.0';
2854-
2855-
return `
2856-
<tr>
2857-
<td class="metric-label">
2858-
${this.getEditorIcon(editor)} ${editor}
2859-
</td>
2860-
<td class="today-value">
2861-
${todayUsage.tokens.toLocaleString()}
2862-
<div style="font-size: 10px; color: #999; font-weight: normal; margin-top: 2px;">${todayPercent}% · ${todayUsage.sessions} sessions</div>
2863-
</td>
2864-
<td class="month-value">
2865-
${monthUsage.tokens.toLocaleString()}
2866-
<div style="font-size: 10px; color: #999; font-weight: normal; margin-top: 2px;">${monthPercent}% · ${monthUsage.sessions} sessions</div>
2867-
</td>
2868-
</tr>
2869-
`;
2870-
}).join('');
2871-
2872-
return `
2873-
<div style="margin-top: 16px;">
2874-
<h3 style="color: #ffffff; font-size: 14px; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;">
2875-
<span>💻</span>
2876-
<span>Usage by Editor</span>
2877-
</h3>
2878-
<table class="stats-table">
2879-
<colgroup>
2880-
<col class="metric-col">
2881-
<col class="value-col">
2882-
<col class="value-col">
2883-
</colgroup>
2884-
<thead>
2885-
<tr>
2886-
<th>Editor</th>
2887-
<th>
2888-
<div class="period-header">
2889-
<span>📅</span>
2890-
<span>Today</span>
2891-
</div>
2892-
</th>
2893-
<th>
2894-
<div class="period-header">
2895-
<span>📊</span>
2896-
<span>This Month</span>
2897-
</div>
2898-
</th>
2899-
</tr>
2900-
</thead>
2901-
<tbody>
2902-
${editorRows}
2903-
</tbody>
2904-
</table>
2905-
</div>
2906-
`;
2907-
}
2908-
2909-
private getEditorIcon(editor: string): string {
2910-
const icons: { [key: string]: string } = {
2911-
'VS Code': '💙',
2912-
'VS Code Insiders': '💚',
2913-
'VS Code Exploration': '🧪',
2914-
'VS Code Server': '☁️',
2915-
'VS Code Server (Insiders)': '☁️',
2916-
'VSCodium': '🔷',
2917-
'Cursor': '⚡',
2918-
'Copilot CLI': '🤖',
2919-
'Unknown': '❓'
2920-
};
2921-
return icons[editor] || '📝';
2922-
}
2923-
2924-
private getModelDisplayName(model: string): string {
2925-
const modelNames: { [key: string]: string } = {
2926-
'gpt-4': 'GPT-4',
2927-
'gpt-4.1': 'GPT-4.1',
2928-
'gpt-4o': 'GPT-4o',
2929-
'gpt-4o-mini': 'GPT-4o Mini',
2930-
'gpt-3.5-turbo': 'GPT-3.5 Turbo',
2931-
'gpt-5': 'GPT-5',
2932-
'gpt-5-codex': 'GPT-5 Codex (Preview)',
2933-
'gpt-5-mini': 'GPT-5 Mini',
2934-
'gpt-5.1': 'GPT-5.1',
2935-
'gpt-5.1-codex': 'GPT-5.1 Codex',
2936-
'gpt-5.1-codex-max': 'GPT-5.1 Codex Max',
2937-
'gpt-5.1-codex-mini': 'GPT-5.1 Codex Mini (Preview)',
2938-
'gpt-5.2': 'GPT-5.2',
2939-
'claude-sonnet-3.5': 'Claude Sonnet 3.5',
2940-
'claude-sonnet-3.7': 'Claude Sonnet 3.7',
2941-
'claude-sonnet-4': 'Claude Sonnet 4',
2942-
'claude-sonnet-4.5': 'Claude Sonnet 4.5',
2943-
'claude-haiku': 'Claude Haiku',
2944-
'claude-haiku-4.5': 'Claude Haiku 4.5',
2945-
'claude-opus-4.1': 'Claude Opus 4.1',
2946-
'claude-opus-4.5': 'Claude Opus 4.5',
2947-
'gemini-2.5-pro': 'Gemini 2.5 Pro',
2948-
'gemini-3-flash': 'Gemini 3 Flash',
2949-
'gemini-3-pro': 'Gemini 3 Pro',
2950-
'gemini-3-pro-preview': 'Gemini 3 Pro (Preview)',
2951-
'grok-code-fast-1': 'Grok Code Fast 1',
2952-
'raptor-mini': 'Raptor Mini',
2953-
'o3-mini': 'o3-mini',
2954-
'o4-mini': 'o4-mini (Preview)'
2955-
};
2956-
return modelNames[model] || model;
2957-
}
29582769

29592770
public async generateDiagnosticReport(): Promise<string> {
29602771
this.log('Generating diagnostic report...');
@@ -3503,7 +3314,7 @@ class CopilotTokenTracker implements vscode.Disposable {
35033314
const modelDatasets = Array.from(allModels).map((model, idx) => {
35043315
const color = modelColors[idx % modelColors.length];
35053316
return {
3506-
label: this.getModelDisplayName(model),
3317+
label: getModelDisplayName(model),
35073318
data: dailyStats.map(d => {
35083319
const usage = d.modelUsage[model];
35093320
return usage ? usage.inputTokens + usage.outputTokens : 0;

0 commit comments

Comments
 (0)