From 76b68575ba234a9ee5f20024160e954c87a5a8f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:29:56 +0000 Subject: [PATCH 01/10] Initial plan From bc49872599e4c494830d8a5e7c778d8cdeba262b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:35:43 +0000 Subject: [PATCH 02/10] Extend cache with session details (firstInteraction, lastInteraction, title) and implement cache-based loading Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- package-lock.json | 9 +++- package.json | 2 +- src/extension.ts | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12ef49a..51e599c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.10", - "@types/node": "25.x", + "@types/node": "^25.2.0", "@types/vscode": "^1.108.1", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.42.0", @@ -563,6 +563,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -604,6 +605,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1894,6 +1896,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -2554,6 +2557,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4036,6 +4040,7 @@ "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", @@ -8648,6 +8653,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8883,6 +8889,7 @@ "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 7a8cc3f..9a8a26c 100644 --- a/package.json +++ b/package.json @@ -241,7 +241,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.10", - "@types/node": "25.x", + "@types/node": "^25.2.0", "@types/vscode": "^1.108.1", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.42.0", diff --git a/src/extension.ts b/src/extension.ts index 12861b7..83bc53e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -74,6 +74,9 @@ interface SessionFileCache { mtime: number; // file modification time as timestamp size?: number; // file size in bytes (optional for backward compatibility) usageAnalysis?: SessionUsageAnalysis; // New analysis data + firstInteraction?: string | null; // ISO timestamp of first interaction + lastInteraction?: string | null; // ISO timestamp of last interaction + title?: string; // Session title (customTitle from session file) } // New interfaces for usage analysis @@ -1835,12 +1838,116 @@ class CopilotTokenTracker implements vscode.Disposable { return analysis; } + /** + * Reconstruct SessionFileDetails from cached data without reading the file. + * Returns undefined if cache is not valid or doesn't have all required data. + */ + private async getSessionFileDetailsFromCache(sessionFile: string, stat: fs.Stats): Promise { + const cached = this.getCachedSessionData(sessionFile); + + // Validate cache against file stats + if (!cached || cached.mtime !== stat.mtime.getTime() || cached.size !== stat.size) { + return undefined; + } + + // Check if cache has the required fields (for backward compatibility with old cache) + if (!cached.usageAnalysis?.contextReferences) { + return undefined; + } + + // Reconstruct SessionFileDetails from cache + const details: SessionFileDetails = { + file: sessionFile, + size: cached.size || stat.size, + modified: stat.mtime.toISOString(), + interactions: cached.interactions, + contextReferences: cached.usageAnalysis.contextReferences, + firstInteraction: cached.firstInteraction || null, + lastInteraction: cached.lastInteraction || null, + editorSource: this.detectEditorSource(sessionFile), + title: cached.title + }; + + // Add editor root and name + try { + const parts = sessionFile.split(/[/\\]/); + const userIdx = parts.findIndex(p => p.toLowerCase() === 'user'); + if (userIdx > 0) { + details.editorRoot = parts.slice(0, userIdx).join(require('path').sep); + } else { + details.editorRoot = require('path').dirname(sessionFile); + } + details.editorName = this.getEditorNameFromRoot(details.editorRoot || ''); + } catch (e) { + details.editorRoot = require('path').dirname(sessionFile); + details.editorName = this.getEditorNameFromRoot(details.editorRoot || ''); + } + + return details; + } + + /** + * Update or create cache entry with session file details. + * Merges new detail fields with existing cached data if available. + */ + private async updateCacheWithSessionDetails( + sessionFile: string, + stat: fs.Stats, + details: SessionFileDetails + ): Promise { + // Get existing cache entry if available + const existingCache = this.getCachedSessionData(sessionFile); + + // Create or update cache entry + const cacheEntry: SessionFileCache = { + tokens: existingCache?.tokens || 0, + interactions: details.interactions, + modelUsage: existingCache?.modelUsage || {}, + mtime: stat.mtime.getTime(), + size: stat.size, + usageAnalysis: existingCache?.usageAnalysis || { + toolCalls: { total: 0, byTool: {} }, + modeUsage: { ask: 0, edit: 0, agent: 0 }, + contextReferences: details.contextReferences, + mcpTools: { total: 0, byServer: {}, byTool: {} }, + modelSwitching: { + uniqueModels: [], + modelCount: 0, + switchCount: 0, + tiers: { standard: [], premium: [], unknown: [] }, + hasMixedTiers: false + } + }, + firstInteraction: details.firstInteraction, + lastInteraction: details.lastInteraction, + title: details.title + }; + + // Update the contextReferences in usageAnalysis + if (cacheEntry.usageAnalysis) { + cacheEntry.usageAnalysis.contextReferences = details.contextReferences; + } + + this.setCachedSessionData(sessionFile, cacheEntry, stat.size); + } + /** * Get detailed session file information for diagnostics view. * Analyzes session files to extract interactions, context references, and timestamps. + * Uses cached data when available to avoid re-reading files. */ private async getSessionFileDetails(sessionFile: string): Promise { const stat = await fs.promises.stat(sessionFile); + + // Try to get details from cache first + const cachedDetails = await this.getSessionFileDetailsFromCache(sessionFile, stat); + if (cachedDetails) { + this._cacheHits++; + return cachedDetails; + } + + this._cacheMisses++; + const details: SessionFileDetails = { file: sessionFile, size: stat.size, @@ -1955,6 +2062,10 @@ class CopilotTokenTracker implements vscode.Disposable { details.firstInteraction = new Date(timestamps[0]).toISOString(); details.lastInteraction = new Date(timestamps[timestamps.length - 1]).toISOString(); } + + // Update cache with the details we just collected + await this.updateCacheWithSessionDetails(sessionFile, stat, details); + return details; } @@ -2022,6 +2133,9 @@ class CopilotTokenTracker implements vscode.Disposable { details.lastInteraction = stat.mtime.toISOString(); } } + + // Update cache with the details we just collected + await this.updateCacheWithSessionDetails(sessionFile, stat, details); } catch (error) { this.warn(`Error analyzing session file details for ${sessionFile}: ${error}`); } From 70adb51f3695c560b0e4b53c6c87ad93efe27112 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:37:16 +0000 Subject: [PATCH 03/10] Add cache performance logging for session files loading Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- src/extension.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 83bc53e..6cce7af 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3945,6 +3945,10 @@ class CopilotTokenTracker implements vscode.Disposable { fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - 14); const detailedSessionFiles: SessionFileDetails[] = []; + // Track cache performance for this load operation + const initialCacheHits = this._cacheHits; + const initialCacheMisses = this._cacheMisses; + // Sort files by modification time (most recent first) before taking first 500 // This ensures we prioritize recent sessions regardless of their folder location const fileStats = await Promise.all( @@ -3994,7 +3998,15 @@ class CopilotTokenTracker implements vscode.Disposable { command: 'sessionFilesLoaded', detailedSessionFiles }); - this.log(`Loaded ${detailedSessionFiles.length} session files in background`); + + // Calculate and log cache performance for this operation + const cacheHits = this._cacheHits - initialCacheHits; + const cacheMisses = this._cacheMisses - initialCacheMisses; + const totalAccesses = cacheHits + cacheMisses; + const hitRate = totalAccesses > 0 ? ((cacheHits / totalAccesses) * 100).toFixed(1) : '0'; + + this.log(`Loaded ${detailedSessionFiles.length} session files in background (Cache: ${cacheHits} hits, ${cacheMisses} misses, ${hitRate}% hit rate)`); + // Mark diagnostics as loaded so we don't reload unnecessarily if (panel === this.diagnosticsPanel) { this.diagnosticsHasLoadedFiles = true; From 8b8ff7fd248f9234d68fb74d71d664c381a58052 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:39:22 +0000 Subject: [PATCH 04/10] Refactor: Extract duplicated editor info logic into helper method Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- src/extension.ts | 48 +++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 6cce7af..8571b3a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1838,6 +1838,25 @@ class CopilotTokenTracker implements vscode.Disposable { return analysis; } + /** + * Add editor root and name information to session file details. + */ + private addEditorInfoToDetails(sessionFile: string, details: SessionFileDetails): void { + try { + const parts = sessionFile.split(/[/\\]/); + const userIdx = parts.findIndex(p => p.toLowerCase() === 'user'); + if (userIdx > 0) { + details.editorRoot = parts.slice(0, userIdx).join(require('path').sep); + } else { + details.editorRoot = require('path').dirname(sessionFile); + } + details.editorName = this.getEditorNameFromRoot(details.editorRoot || ''); + } catch (e) { + details.editorRoot = require('path').dirname(sessionFile); + details.editorName = this.getEditorNameFromRoot(details.editorRoot || ''); + } + } + /** * Reconstruct SessionFileDetails from cached data without reading the file. * Returns undefined if cache is not valid or doesn't have all required data. @@ -1869,19 +1888,7 @@ class CopilotTokenTracker implements vscode.Disposable { }; // Add editor root and name - try { - const parts = sessionFile.split(/[/\\]/); - const userIdx = parts.findIndex(p => p.toLowerCase() === 'user'); - if (userIdx > 0) { - details.editorRoot = parts.slice(0, userIdx).join(require('path').sep); - } else { - details.editorRoot = require('path').dirname(sessionFile); - } - details.editorName = this.getEditorNameFromRoot(details.editorRoot || ''); - } catch (e) { - details.editorRoot = require('path').dirname(sessionFile); - details.editorName = this.getEditorNameFromRoot(details.editorRoot || ''); - } + this.addEditorInfoToDetails(sessionFile, details); return details; } @@ -1963,20 +1970,7 @@ class CopilotTokenTracker implements vscode.Disposable { }; // Determine top-level editor root path for this session file (up to the folder before 'User') - try { - const parts = sessionFile.split(/[/\\\\]/); - const userIdx = parts.findIndex(p => p.toLowerCase() === 'user'); - if (userIdx > 0) { - details.editorRoot = parts.slice(0, userIdx).join(require('path').sep); - } else { - details.editorRoot = require('path').dirname(sessionFile); - } - // Also populate a friendly editor name for this file - details['editorName'] = this.getEditorNameFromRoot(details.editorRoot || ''); - } catch (e) { - details.editorRoot = require('path').dirname(sessionFile); - details['editorName'] = this.getEditorNameFromRoot(details.editorRoot || ''); - } + this.addEditorInfoToDetails(sessionFile, details); try { const fileContent = await fs.promises.readFile(sessionFile, 'utf8'); From 77300d2ce63712f1aacf17c6caa59687045ea842 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:40:51 +0000 Subject: [PATCH 05/10] Address code review: Improve method naming, validation, and consistency Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- src/extension.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 8571b3a..a81f297 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1840,8 +1840,9 @@ class CopilotTokenTracker implements vscode.Disposable { /** * Add editor root and name information to session file details. + * Enriches the details object with editorRoot and editorName properties. */ - private addEditorInfoToDetails(sessionFile: string, details: SessionFileDetails): void { + private enrichDetailsWithEditorInfo(sessionFile: string, details: SessionFileDetails): void { try { const parts = sessionFile.split(/[/\\]/); const userIdx = parts.findIndex(p => p.toLowerCase() === 'user'); @@ -1870,7 +1871,7 @@ class CopilotTokenTracker implements vscode.Disposable { } // Check if cache has the required fields (for backward compatibility with old cache) - if (!cached.usageAnalysis?.contextReferences) { + if (!cached.usageAnalysis?.contextReferences || typeof cached.interactions !== 'number') { return undefined; } @@ -1888,7 +1889,7 @@ class CopilotTokenTracker implements vscode.Disposable { }; // Add editor root and name - this.addEditorInfoToDetails(sessionFile, details); + this.enrichDetailsWithEditorInfo(sessionFile, details); return details; } @@ -1970,7 +1971,7 @@ class CopilotTokenTracker implements vscode.Disposable { }; // Determine top-level editor root path for this session file (up to the folder before 'User') - this.addEditorInfoToDetails(sessionFile, details); + this.enrichDetailsWithEditorInfo(sessionFile, details); try { const fileContent = await fs.promises.readFile(sessionFile, 'utf8'); @@ -3997,7 +3998,7 @@ class CopilotTokenTracker implements vscode.Disposable { const cacheHits = this._cacheHits - initialCacheHits; const cacheMisses = this._cacheMisses - initialCacheMisses; const totalAccesses = cacheHits + cacheMisses; - const hitRate = totalAccesses > 0 ? ((cacheHits / totalAccesses) * 100).toFixed(1) : '0'; + const hitRate = totalAccesses > 0 ? ((cacheHits / totalAccesses) * 100).toFixed(1) : '0.0'; this.log(`Loaded ${detailedSessionFiles.length} session files in background (Cache: ${cacheHits} hits, ${cacheMisses} misses, ${hitRate}% hit rate)`); From 0e3dd36409c81ae0f56720198b49935e82ab3fda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 06:42:40 +0000 Subject: [PATCH 06/10] Fix validation and improve code consistency Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- src/extension.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index a81f297..65dbcd2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1871,7 +1871,7 @@ class CopilotTokenTracker implements vscode.Disposable { } // Check if cache has the required fields (for backward compatibility with old cache) - if (!cached.usageAnalysis?.contextReferences || typeof cached.interactions !== 'number') { + if (!cached.usageAnalysis?.contextReferences || typeof cached.interactions !== 'number' || cached.interactions < 0) { return undefined; } @@ -1916,7 +1916,10 @@ class CopilotTokenTracker implements vscode.Disposable { usageAnalysis: existingCache?.usageAnalysis || { toolCalls: { total: 0, byTool: {} }, modeUsage: { ask: 0, edit: 0, agent: 0 }, - contextReferences: details.contextReferences, + contextReferences: { + file: 0, selection: 0, implicitSelection: 0, symbol: 0, codebase: 0, + workspace: 0, terminal: 0, vscode: 0 + }, mcpTools: { total: 0, byServer: {}, byTool: {} }, modelSwitching: { uniqueModels: [], @@ -1931,10 +1934,9 @@ class CopilotTokenTracker implements vscode.Disposable { title: details.title }; - // Update the contextReferences in usageAnalysis - if (cacheEntry.usageAnalysis) { - cacheEntry.usageAnalysis.contextReferences = details.contextReferences; - } + // Update the contextReferences in usageAnalysis with the current data + // usageAnalysis is guaranteed to exist here since we always initialize it above + cacheEntry.usageAnalysis!.contextReferences = details.contextReferences; this.setCachedSessionData(sessionFile, cacheEntry, stat.size); } From 884790ecf554f1de4f7e6ae000a14826f55c1c66 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Sat, 7 Feb 2026 17:30:34 +0100 Subject: [PATCH 07/10] Adding more tool names --- src/toolNames.json | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/toolNames.json b/src/toolNames.json index 232c720..7165c72 100644 --- a/src/toolNames.json +++ b/src/toolNames.json @@ -1,17 +1,21 @@ { - "unknown": "Unknown", - "copilot_readFile": "Read File", - "copilot_applyPatch": "Apply Patch", - "copilot_findTextInFiles": "Find Text In Files", - "run_in_terminal": "Run In Terminal", - "mcp.io.github.git.assign_copilot_to_issue": "Assign Copilot to Issue", - "mcp.io.github.git.create_or_update_file": "Git: Create/Update File" + "unknown": "Unknown" + ,"run_in_terminal": "Run In Terminal" + ,"mcp.io.github.git.assign_copilot_to_issue": "GitHub MCP: Assign Copilot to Issue" + ,"mcp.io.github.git.create_or_update_file": "GitHub MCP: Create/Update File" + ,"mcp_io_github_git_pull_request_read": "GitHub MCP: Pull Request Read" ,"manage_todo_list": "Manage TODO List" + ,"copilot_readFile": "Read File" + ,"copilot_applyPatch": "Apply Patch" + ,"copilot_findTextInFiles": "Find Text In Files" ,"copilot_replaceString": "Replace String" ,"copilot_createFile": "Create File" ,"copilot_listDirectory": "List Directory" ,"copilot_fetchWebPage": "Fetch Web Page" ,"copilot_getErrors": "Get Errors" ,"copilot_multiReplaceString": "Multi Replace String" + ,"copilot_searchCodebase": "Search Codebase" ,"get_terminal_output": "Get Terminal Output" + ,"run_task": "Run Task: Investigate" + ,"await_terminal": "Await Terminal command" } From a54199a06abcef4dc1b452d52b8a2f143b76b947 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Sat, 7 Feb 2026 17:30:49 +0100 Subject: [PATCH 08/10] Update session files overview --- src/extension.ts | 29 ++++++++++++++++++++++++- src/webview/diagnostics/main.ts | 38 ++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 8c961d9..d34821a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -236,6 +236,10 @@ class CopilotTokenTracker implements vscode.Disposable { private waterUsagePer1kTokens = 0.3; // liters of water per 1000 tokens, based on data center usage estimates private _cacheHits = 0; // Counter for cache hits during usage analysis private _cacheMisses = 0; // Counter for cache misses during usage analysis + // Short-term cache to avoid rescanning filesystem during rapid successive calls (e.g., diagnostics load) + private _sessionFilesCache: string[] | null = null; + private _sessionFilesCacheTime: number = 0; + private static readonly SESSION_FILES_CACHE_TTL = 60000; // Cache for 60 seconds // Model pricing data - loaded from modelPricing.json // Reference: OpenAI API Pricing (https://openai.com/api/pricing/) - Retrieved December 2025 @@ -2069,7 +2073,9 @@ class CopilotTokenTracker implements vscode.Disposable { modeUsage: { ask: 0, edit: 0, agent: 0 }, contextReferences: { file: 0, selection: 0, implicitSelection: 0, symbol: 0, codebase: 0, - workspace: 0, terminal: 0, vscode: 0 + workspace: 0, terminal: 0, vscode: 0, + // Extended fields expected by SessionUsageAnalysis in the webview + byKind: {}, copilotInstructions: 0, agentsMd: 0, byPath: {} }, mcpTools: { total: 0, byServer: {}, byTool: {} }, modelSwitching: { @@ -2734,6 +2740,13 @@ class CopilotTokenTracker implements vscode.Disposable { * This TypeScript implementation should mirror that logic. */ private async getCopilotSessionFiles(): Promise { + // Check short-term cache to avoid expensive filesystem scans during rapid successive calls + const now = Date.now(); + if (this._sessionFilesCache && (now - this._sessionFilesCacheTime) < CopilotTokenTracker.SESSION_FILES_CACHE_TTL) { + this.log(`💨 Using cached session files list (${this._sessionFilesCache.length} files, cached ${Math.round((now - this._sessionFilesCacheTime) / 1000)}s ago)`); + return this._sessionFilesCache; + } + const sessionFiles: string[] = []; const platform = os.platform(); @@ -2862,6 +2875,10 @@ class CopilotTokenTracker implements vscode.Disposable { if (sessionFiles.length === 0) { this.warn('⚠️ No session files found - Have you used GitHub Copilot Chat yet?'); } + + // Update short-term cache + this._sessionFilesCache = sessionFiles; + this._sessionFilesCacheTime = Date.now(); } catch (error) { this.error('Error getting session files:', error); } @@ -4091,6 +4108,16 @@ class CopilotTokenTracker implements vscode.Disposable { try { this.log('🔄 Loading diagnostic data in background...'); + // CRITICAL: Ensure stats have been calculated at least once to populate cache + // If this is the first diagnostic panel open and no stats exist yet, + // force an update now so the cache is populated before we load session files. + // This dramatically improves performance on first load (near 100% cache hit rate). + if (!this.lastDetailedStats) { + this.log('⚡ No cached stats found - forcing initial stats calculation to populate cache...'); + await this.updateTokenStats(true); + this.log('✅ Cache populated, proceeding with diagnostics load'); + } + // Load the diagnostic report const report = await this.generateDiagnosticReport(); this.lastDiagnosticReport = report; diff --git a/src/webview/diagnostics/main.ts b/src/webview/diagnostics/main.ts index 991726b..5233f5a 100644 --- a/src/webview/diagnostics/main.ts +++ b/src/webview/diagnostics/main.ts @@ -506,6 +506,9 @@ function renderLayout(data: DiagnosticsData): void { `; + const totalSessions = sorted.reduce((sum, sf) => sum + sf.count, 0); + console.log('[Diagnostics] Total sessions calculated:', totalSessions, 'from', sorted.length, 'folders'); + sorted.forEach((sf: { dir: string; count: number; editorName?: string }) => { // Shorten common user paths for readability let display = sf.dir; @@ -522,6 +525,16 @@ function renderLayout(data: DiagnosticsData): void { Open directory `; }); + + // Add total row + sessionFilesHtml += ` + + Total: + ${totalSessions} + + `; + console.log('[Diagnostics] Total row HTML added to sessionFilesHtml'); + sessionFilesHtml += ` @@ -1014,7 +1027,30 @@ function renderLayout(data: DiagnosticsData): void { tbody.appendChild(row); }); - // Find where to insert or replace the session folders table + // Add total row + const totalSessions = sorted.reduce((sum, sf) => sum + sf.count, 0); + const totalRow = document.createElement('tr'); + totalRow.style.borderTop = '2px solid var(--vscode-panel-border)'; + totalRow.style.fontWeight = 'bold'; + totalRow.style.background = 'rgba(255, 255, 255, 0.05)'; + + const totalLabelCell = document.createElement('td'); + totalLabelCell.setAttribute('colspan', '2'); + totalLabelCell.style.textAlign = 'right'; + totalLabelCell.style.paddingRight = '16px'; + totalLabelCell.textContent = 'Total:'; + totalRow.appendChild(totalLabelCell); + + const totalCountCell = document.createElement('td'); + totalCountCell.textContent = totalSessions.toString(); + totalRow.appendChild(totalCountCell); + + const totalEmptyCell = document.createElement('td'); + totalRow.appendChild(totalEmptyCell); + + tbody.appendChild(totalRow); + + // Find where to insert or replace the session folders table // It should be inserted after the report-content div but before the button-group const existingTable = reportTabContent.querySelector('.session-folders-table'); if (!existingTable) { From 933858ae26bb413937a3fb0eb2554efead22b257 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Sat, 7 Feb 2026 18:13:36 +0100 Subject: [PATCH 09/10] Fix loading of the session titles --- src/extension.ts | 129 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 88 insertions(+), 41 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index d34821a..a727183 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -204,7 +204,7 @@ interface SessionLogData { class CopilotTokenTracker implements vscode.Disposable { // Cache version - increment this when making changes that require cache invalidation - private static readonly CACHE_VERSION = 11; // Fix toolId extraction for tool calls (2026-02-05) + private static readonly CACHE_VERSION = 14; // Comprehensive title extraction: all response item types + kind:1 updates (2026-02-07) private diagnosticsPanel?: vscode.WebviewPanel; // Tracks whether the diagnostics panel has already received its session files @@ -433,7 +433,6 @@ class CopilotTokenTracker implements vscode.Disposable { try { // Show the output channel so users can see what's happening this.outputChannel.show(true); - this.log('DEBUG clearCache() called'); this.log('Clearing session file cache...'); const cacheSize = this.sessionFileCache.size; @@ -1903,8 +1902,77 @@ class CopilotTokenTracker implements vscode.Disposable { // Note: We don't add to byPath here as these are automatic attachments, // not explicit user file selections } + } + } + } + + /** + * Extract session metadata (title, timestamps) from a session file. + * Used to populate cache with information needed for session file details. + */ + private async extractSessionMetadata(sessionFile: string): Promise<{ + title: string | undefined; + firstInteraction: string | null; + lastInteraction: string | null; + }> { + let title: string | undefined; + const timestamps: number[] = []; + + try { + const fileContent = await fs.promises.readFile(sessionFile, 'utf8'); + const isJsonlContent = sessionFile.endsWith('.jsonl') || this.isJsonlContent(fileContent); + + if (isJsonlContent) { + const lines = fileContent.trim().split('\n'); + for (const line of lines) { + if (!line.trim()) { continue; } + try { + const event = JSON.parse(line); + + // Handle Copilot CLI format + if (event.type === 'user.message') { + const ts = event.timestamp || event.ts || event.data?.timestamp; + if (ts) { timestamps.push(new Date(ts).getTime()); } + } + + // Handle VS Code incremental .jsonl format + if (event.kind === 0 && event.v) { + if (event.v.creationDate) { timestamps.push(event.v.creationDate); } + // Always update title - we want the LAST title in the file (matches VS Code UI) + if (event.v.customTitle) { title = event.v.customTitle; } + } + + // Check kind: 1 (value updates) for title changes + if (event.kind === 1 && event.k?.includes('customTitle') && event.v) { + title = event.v; + } + } catch { + // Skip malformed lines + } + } + } else { + // JSON format - try to parse + try { + const parsed = JSON.parse(fileContent); + if (parsed.customTitle) { title = parsed.customTitle; } + if (parsed.creationDate) { timestamps.push(parsed.creationDate); } + } catch { + // Unable to parse + } } + } catch { + // File read error + } + + let firstInteraction: string | null = null; + let lastInteraction: string | null = null; + if (timestamps.length > 0) { + timestamps.sort((a, b) => a - b); + firstInteraction = new Date(timestamps[0]).toISOString(); + lastInteraction = new Date(timestamps[timestamps.length - 1]).toISOString(); } + + return { title, firstInteraction, lastInteraction }; } // Cached versions of session file reading methods @@ -1923,12 +1991,19 @@ class CopilotTokenTracker implements vscode.Disposable { const modelUsage = await this.getModelUsageFromSession(sessionFilePath); const usageAnalysis = await this.analyzeSessionUsage(sessionFilePath); + // Extract title and timestamps from the session file + const sessionMeta = await this.extractSessionMetadata(sessionFilePath); + const sessionData: SessionFileCache = { tokens, interactions, modelUsage, mtime, - usageAnalysis + size: fileSize, + usageAnalysis, + title: sessionMeta.title, + firstInteraction: sessionMeta.firstInteraction, + lastInteraction: sessionMeta.lastInteraction }; this.setCachedSessionData(sessionFilePath, sessionData, fileSize); @@ -2167,8 +2242,8 @@ class CopilotTokenTracker implements vscode.Disposable { if (event.v.creationDate) { timestamps.push(event.v.creationDate); } - // Session title - if (event.v.customTitle && !details.title) { + // Session title - always update to get LAST title (matches VS Code UI) + if (event.v.customTitle) { details.title = event.v.customTitle; } } @@ -2186,26 +2261,13 @@ class CopilotTokenTracker implements vscode.Disposable { if (request.message?.text) { this.analyzeContextReferences(request.message.text, details.contextReferences); } - // Fallback: look for generatedTitle in response items - if (!details.title && request.response && Array.isArray(request.response)) { - for (const responseItem of request.response) { - if (responseItem.generatedTitle) { - details.title = responseItem.generatedTitle; - break; - } - } - } - } - } - - // Also check kind: 2 events that update response arrays directly - if (!details.title && event.kind === 2 && event.k?.includes('response') && Array.isArray(event.v)) { - for (const responseItem of event.v) { - if (responseItem.generatedTitle) { - details.title = responseItem.generatedTitle; - break; - } + } + } + + // Check kind: 1 (value updates) for title changes + if (event.kind === 1 && event.k?.includes('customTitle') && event.v) { + details.title = event.v; } } catch { // Skip malformed lines @@ -2230,22 +2292,7 @@ class CopilotTokenTracker implements vscode.Disposable { // Extract session title if available if (sessionContent.customTitle) { details.title = sessionContent.customTitle; - } - - // Fallback: look for generatedTitle in responses if no customTitle - if (!details.title && sessionContent.requests && Array.isArray(sessionContent.requests)) { - for (const request of sessionContent.requests) { - if (details.title) { break; } - if (request.response && Array.isArray(request.response)) { - for (const responseItem of request.response) { - if (responseItem.generatedTitle) { - details.title = responseItem.generatedTitle; - break; - } - } - } - } - } + } if (sessionContent.requests && Array.isArray(sessionContent.requests)) { details.interactions = sessionContent.requests.length; @@ -4056,7 +4103,7 @@ class CopilotTokenTracker implements vscode.Disposable { await this.showUsageAnalysis(); break; case 'clearCache': - this.log('DEBUG clearCache message received from diagnostics webview'); + this.log('clearCache message received from diagnostics webview'); await this.clearCache(); // After clearing cache, refresh the diagnostic report if it's open if (this.diagnosticsPanel) { From 9df90cd73535d86d56725448cfb633d11b283c1b Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Sat, 7 Feb 2026 18:41:36 +0100 Subject: [PATCH 10/10] Fix loading the session numbers for today --- src/extension.ts | 54 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index a727183..2a71349 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1940,7 +1940,16 @@ class CopilotTokenTracker implements vscode.Disposable { if (event.v.creationDate) { timestamps.push(event.v.creationDate); } // Always update title - we want the LAST title in the file (matches VS Code UI) if (event.v.customTitle) { title = event.v.customTitle; } - } + } + + // Handle kind: 2 events (requests array with timestamps) + if (event.kind === 2 && event.k?.[0] === 'requests' && Array.isArray(event.v)) { + for (const request of event.v) { + if (request.timestamp) { + timestamps.push(request.timestamp); + } + } + } // Check kind: 1 (value updates) for title changes if (event.kind === 1 && event.k?.includes('customTitle') && event.v) { @@ -1955,7 +1964,16 @@ class CopilotTokenTracker implements vscode.Disposable { try { const parsed = JSON.parse(fileContent); if (parsed.customTitle) { title = parsed.customTitle; } - if (parsed.creationDate) { timestamps.push(parsed.creationDate); } + if (parsed.creationDate) { timestamps.push(parsed.creationDate); } + // Extract timestamps from requests array (like getSessionFileDetails does) + if (parsed.requests && Array.isArray(parsed.requests)) { + for (const request of parsed.requests) { + if (request.timestamp || request.ts || request.result?.timestamp) { + const ts = request.timestamp || request.ts || request.result?.timestamp; + timestamps.push(new Date(ts).getTime()); + } + } + } } catch { // Unable to parse } @@ -2105,6 +2123,19 @@ class CopilotTokenTracker implements vscode.Disposable { return undefined; } + // Determine lastInteraction: use the more recent of cached timestamp or file mtime + // This handles cases where file was modified but content timestamps are older + let lastInteraction: string | null = cached.lastInteraction || null; + if (lastInteraction) { + const cachedLastInteraction = new Date(lastInteraction); + if (stat.mtime > cachedLastInteraction) { + lastInteraction = stat.mtime.toISOString(); + } + } else { + // No cached lastInteraction, use file mtime + lastInteraction = stat.mtime.toISOString(); + } + // Reconstruct SessionFileDetails from cache const details: SessionFileDetails = { file: sessionFile, @@ -2113,7 +2144,7 @@ class CopilotTokenTracker implements vscode.Disposable { interactions: cached.interactions, contextReferences: cached.usageAnalysis.contextReferences, firstInteraction: cached.firstInteraction || null, - lastInteraction: cached.lastInteraction || null, + lastInteraction: lastInteraction, editorSource: this.detectEditorSource(sessionFile), title: cached.title }; @@ -2277,7 +2308,15 @@ class CopilotTokenTracker implements vscode.Disposable { if (timestamps.length > 0) { timestamps.sort((a, b) => a - b); details.firstInteraction = new Date(timestamps[0]).toISOString(); - details.lastInteraction = new Date(timestamps[timestamps.length - 1]).toISOString(); + // Use the more recent of: extracted last timestamp OR file modification time + // This handles cases where new requests are added without timestamp fields + const lastTimestamp = new Date(timestamps[timestamps.length - 1]); + details.lastInteraction = lastTimestamp > stat.mtime + ? lastTimestamp.toISOString() + : stat.mtime.toISOString(); + } else { + // Fallback to file modification time if no timestamps in content + details.lastInteraction = stat.mtime.toISOString(); } // Update cache with the details we just collected @@ -2329,7 +2368,12 @@ class CopilotTokenTracker implements vscode.Disposable { if (timestamps.length > 0) { timestamps.sort((a, b) => a - b); details.firstInteraction = new Date(timestamps[0]).toISOString(); - details.lastInteraction = new Date(timestamps[timestamps.length - 1]).toISOString(); + // Use the more recent of: extracted last timestamp OR file modification time + // This handles cases where new requests are added without timestamp fields + const lastTimestamp = new Date(timestamps[timestamps.length - 1]); + details.lastInteraction = lastTimestamp > stat.mtime + ? lastTimestamp.toISOString() + : stat.mtime.toISOString(); } else { // Fallback to file modification time if no timestamps in content details.lastInteraction = stat.mtime.toISOString();