Skip to content
Merged
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
153 changes: 138 additions & 15 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@
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
Expand Down Expand Up @@ -1986,12 +1989,126 @@
return analysis;
}

/**
* Add editor root and name information to session file details.
* Enriches the details object with editorRoot and editorName properties.
*/
private enrichDetailsWithEditorInfo(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.
*/
private async getSessionFileDetailsFromCache(sessionFile: string, stat: fs.Stats): Promise<SessionFileDetails | undefined> {
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 || typeof cached.interactions !== 'number' || cached.interactions < 0) {
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
this.enrichDetailsWithEditorInfo(sessionFile, details);

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<void> {
// 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 || {

Check failure on line 2067 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build-and-test (windows-latest, 18.x)

Type 'SessionUsageAnalysis | { toolCalls: { total: number; byTool: {}; }; modeUsage: { ask: number; edit: number; agent: number; }; contextReferences: { file: number; selection: number; ... 5 more ...; vscode: number; }; mcpTools: { ...; }; modelSwitching: { ...; }; }' is not assignable to type 'SessionUsageAnalysis | undefined'.

Check failure on line 2067 in src/extension.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Type 'SessionUsageAnalysis | { toolCalls: { total: number; byTool: {}; }; modeUsage: { ask: number; edit: number; agent: number; }; contextReferences: { file: number; selection: number; ... 5 more ...; vscode: number; }; mcpTools: { ...; }; modelSwitching: { ...; }; }' is not assignable to type 'SessionUsageAnalysis | undefined'.
toolCalls: { total: 0, byTool: {} },
modeUsage: { ask: 0, edit: 0, agent: 0 },
contextReferences: {
file: 0, selection: 0, implicitSelection: 0, symbol: 0, codebase: 0,
workspace: 0, terminal: 0, vscode: 0
},
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 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);
}

/**
* 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<SessionFileDetails> {
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,
Expand All @@ -2008,20 +2125,7 @@
};

// 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.enrichDetailsWithEditorInfo(sessionFile, details);

try {
const fileContent = await fs.promises.readFile(sessionFile, 'utf8');
Expand Down Expand Up @@ -2107,6 +2211,10 @@
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;
}

Expand Down Expand Up @@ -2174,6 +2282,9 @@
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}`);
}
Expand Down Expand Up @@ -4076,6 +4187,10 @@
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(
Expand Down Expand Up @@ -4125,7 +4240,15 @@
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.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;
Expand Down
Loading