diff --git a/visualstudio-extension/src/CopilotTokenTracker/source.extension.vsixmanifest b/visualstudio-extension/src/CopilotTokenTracker/source.extension.vsixmanifest
index 35053ee..a9aae71 100644
--- a/visualstudio-extension/src/CopilotTokenTracker/source.extension.vsixmanifest
+++ b/visualstudio-extension/src/CopilotTokenTracker/source.extension.vsixmanifest
@@ -9,8 +9,8 @@
Publisher="Rob Bos" />
AI Engineering Fluency
Measure and grow your AI engineering fluency in Visual Studio. Tracks GitHub Copilot token usage, today's and last-30-days activity, per-model breakdowns, and detailed session analysis.
- GitHub Copilot, token usage, AI, productivity
assets\logo.png
+ GitHub Copilot, token usage, AI, productivity
diff --git a/vscode-extension/src/sessionDiscovery.ts b/vscode-extension/src/sessionDiscovery.ts
index b86a70e..5d2bcf0 100644
--- a/vscode-extension/src/sessionDiscovery.ts
+++ b/vscode-extension/src/sessionDiscovery.ts
@@ -37,6 +37,16 @@ export class SessionDiscovery {
this._sessionFilesCacheTime = 0;
}
+ /** Async replacement for fs.existsSync — does not block the event loop. */
+ private async pathExists(p: string): Promise {
+ try {
+ await fs.promises.access(p);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
/**
* Get all possible VS Code user data paths for all VS Code variants
* Supports: Code (stable), Code - Insiders, VSCodium, remote servers, etc.
@@ -232,7 +242,7 @@ export class SessionDiscovery {
for (let i = 0; i < allVSCodePaths.length; i++) {
const codeUserPath = allVSCodePaths[i];
try {
- if (fs.existsSync(codeUserPath)) {
+ if (await this.pathExists(codeUserPath)) {
foundPaths.push(codeUserPath);
}
} catch (checkError) {
@@ -258,16 +268,16 @@ export class SessionDiscovery {
// Workspace storage sessions
const workspaceStoragePath = path.join(codeUserPath, 'workspaceStorage');
try {
- if (fs.existsSync(workspaceStoragePath)) {
+ if (await this.pathExists(workspaceStoragePath)) {
try {
- const workspaceDirs = fs.readdirSync(workspaceStoragePath);
+ const workspaceDirs = await fs.promises.readdir(workspaceStoragePath);
for (const workspaceDir of workspaceDirs) {
const chatSessionsPath = path.join(workspaceStoragePath, workspaceDir, 'chatSessions');
try {
- if (fs.existsSync(chatSessionsPath)) {
+ if (await this.pathExists(chatSessionsPath)) {
try {
- const sessionFiles2 = fs.readdirSync(chatSessionsPath)
+ const sessionFiles2 = (await fs.promises.readdir(chatSessionsPath))
.filter(file => file.endsWith('.json') || file.endsWith('.jsonl'))
.map(file => path.join(chatSessionsPath, file));
if (sessionFiles2.length > 0) {
@@ -293,9 +303,9 @@ export class SessionDiscovery {
// Global storage sessions (legacy emptyWindowChatSessions)
const globalStoragePath = path.join(codeUserPath, 'globalStorage', 'emptyWindowChatSessions');
try {
- if (fs.existsSync(globalStoragePath)) {
+ if (await this.pathExists(globalStoragePath)) {
try {
- const globalSessionFiles = fs.readdirSync(globalStoragePath)
+ const globalSessionFiles = (await fs.promises.readdir(globalStoragePath))
.filter(file => file.endsWith('.json') || file.endsWith('.jsonl'))
.map(file => path.join(globalStoragePath, file));
if (globalSessionFiles.length > 0) {
@@ -313,9 +323,9 @@ export class SessionDiscovery {
// GitHub Copilot Chat extension global storage
const copilotChatGlobalPath = path.join(codeUserPath, 'globalStorage', 'github.copilot-chat');
try {
- if (fs.existsSync(copilotChatGlobalPath)) {
+ if (await this.pathExists(copilotChatGlobalPath)) {
this.deps.log(`📄 Scanning ${pathName}/globalStorage/github.copilot-chat`);
- this.scanDirectoryForSessionFiles(copilotChatGlobalPath, sessionFiles);
+ await this.scanDirectoryForSessionFiles(copilotChatGlobalPath, sessionFiles);
}
} catch (checkError) {
this.deps.warn(`Could not check Copilot Chat global storage path ${copilotChatGlobalPath}: ${checkError}`);
@@ -324,11 +334,11 @@ export class SessionDiscovery {
// Check for Copilot CLI session-state directory (new location for agent mode sessions)
const copilotCliSessionPath = path.join(os.homedir(), '.copilot', 'session-state');
- this.deps.log(`📁 Checking Copilot CLI path: ${copilotCliSessionPath} (exists: ${fs.existsSync(copilotCliSessionPath)})`);
+ this.deps.log(`📁 Checking Copilot CLI path: ${copilotCliSessionPath}`);
try {
- if (fs.existsSync(copilotCliSessionPath)) {
+ if (await this.pathExists(copilotCliSessionPath)) {
try {
- const entries = fs.readdirSync(copilotCliSessionPath, { withFileTypes: true });
+ const entries = await fs.promises.readdir(copilotCliSessionPath, { withFileTypes: true });
// Collect flat .json/.jsonl files at the top level
const cliSessionFiles = entries
@@ -345,12 +355,10 @@ export class SessionDiscovery {
for (const subDir of subDirs) {
const eventsFile = path.join(copilotCliSessionPath, subDir.name, 'events.jsonl');
try {
- if (fs.existsSync(eventsFile)) {
- const stats = fs.statSync(eventsFile);
- if (stats.size > 0) {
- sessionFiles.push(eventsFile);
- subDirSessionCount++;
- }
+ const stats = await fs.promises.stat(eventsFile);
+ if (stats.size > 0) {
+ sessionFiles.push(eventsFile);
+ subDirSessionCount++;
}
} catch {
// Ignore individual file access errors
@@ -372,20 +380,20 @@ export class SessionDiscovery {
const openCodeDataDir = this.deps.openCode.getOpenCodeDataDir();
const openCodeSessionDir = path.join(openCodeDataDir, 'storage', 'session');
const openCodeDbPath = path.join(openCodeDataDir, 'opencode.db');
- this.deps.log(`📁 Checking OpenCode JSON path: ${openCodeSessionDir} (exists: ${fs.existsSync(openCodeSessionDir)})`);
- this.deps.log(`📁 Checking OpenCode DB path: ${openCodeDbPath} (exists: ${fs.existsSync(openCodeDbPath)})`);
+ this.deps.log(`📁 Checking OpenCode JSON path: ${openCodeSessionDir}`);
+ this.deps.log(`📁 Checking OpenCode DB path: ${openCodeDbPath}`);
try {
- if (fs.existsSync(openCodeSessionDir)) {
- const scanOpenCodeDir = (dir: string) => {
+ if (await this.pathExists(openCodeSessionDir)) {
+ const scanOpenCodeDir = async (dir: string) => {
try {
- const entries = fs.readdirSync(dir, { withFileTypes: true });
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
- scanOpenCodeDir(path.join(dir, entry.name));
+ await scanOpenCodeDir(path.join(dir, entry.name));
} else if (entry.name.startsWith('ses_') && entry.name.endsWith('.json')) {
const fullPath = path.join(dir, entry.name);
try {
- const stats = fs.statSync(fullPath);
+ const stats = await fs.promises.stat(fullPath);
if (stats.size > 0) {
sessionFiles.push(fullPath);
}
@@ -398,7 +406,7 @@ export class SessionDiscovery {
// Ignore directory access errors
}
};
- scanOpenCodeDir(openCodeSessionDir);
+ await scanOpenCodeDir(openCodeSessionDir);
const openCodeCount = sessionFiles.length - (sessionFiles.filter(f => !this.deps.openCode.isOpenCodeSessionFile(f))).length;
if (openCodeCount > 0) {
this.deps.log(`📄 Found ${openCodeCount} session files in OpenCode storage`);
@@ -411,7 +419,7 @@ export class SessionDiscovery {
// Check for OpenCode sessions in SQLite database (opencode.db)
// Newer OpenCode versions store sessions in SQLite instead of JSON files
try {
- if (fs.existsSync(openCodeDbPath)) {
+ if (await this.pathExists(openCodeDbPath)) {
const existingSessionIds = new Set(
sessionFiles
.filter(f => this.deps.openCode.isOpenCodeSessionFile(f))
@@ -443,9 +451,9 @@ export class SessionDiscovery {
let crushTotal = 0;
for (const project of crushProjects) {
const dbPath = path.join(project.data_dir, 'crush.db');
- this.deps.log(`📁 Checking Crush DB path: ${dbPath} (exists: ${fs.existsSync(dbPath)})`);
+ this.deps.log(`📁 Checking Crush DB path: ${dbPath}`);
try {
- if (fs.existsSync(dbPath)) {
+ if (await this.pathExists(dbPath)) {
const sessionIds = await this.deps.crush.discoverSessionsInDb(dbPath);
for (const sessionId of sessionIds) {
// Virtual path: /crush.db#
@@ -517,13 +525,13 @@ export class SessionDiscovery {
*
* NOTE: Mirrors logic in .github/skills/copilot-log-analysis/session-file-discovery.js
*/
- scanDirectoryForSessionFiles(dir: string, sessionFiles: string[]): void {
+ async scanDirectoryForSessionFiles(dir: string, sessionFiles: string[]): Promise {
try {
- const entries = fs.readdirSync(dir, { withFileTypes: true });
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
- this.scanDirectoryForSessionFiles(fullPath, sessionFiles);
+ await this.scanDirectoryForSessionFiles(fullPath, sessionFiles);
} else if (entry.name.endsWith('.json') || entry.name.endsWith('.jsonl')) {
// Skip known non-session files (embeddings, indexes, etc.)
if (this.isNonSessionFile(entry.name)) {
@@ -531,7 +539,7 @@ export class SessionDiscovery {
}
// Only add files that look like session files (have reasonable content)
try {
- const stats = fs.statSync(fullPath);
+ const stats = await fs.promises.stat(fullPath);
if (stats.size > 0) {
sessionFiles.push(fullPath);
}