From 8b406c242b7674f03a012b5a3c6deef2a3d8b0f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:15:45 +0000 Subject: [PATCH 01/22] Initial plan From 0eda5fb3cf5156c7238affc57abb48423a974ada Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:19:16 +0000 Subject: [PATCH 02/22] Add GitHub authentication support to extension Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- vscode-extension/package.json | 10 ++++ vscode-extension/src/extension.ts | 88 +++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 56075ef1..db5cbf93 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -124,6 +124,16 @@ "command": "copilot-token-tracker.showEnvironmental", "title": "Show Environmental Impact", "category": "AI Engineering Fluency" + }, + { + "command": "copilot-token-tracker.authenticateGitHub", + "title": "Authenticate with GitHub", + "category": "AI Engineering Fluency" + }, + { + "command": "copilot-token-tracker.signOutGitHub", + "title": "Sign Out from GitHub", + "category": "AI Engineering Fluency" } ], "configuration": { diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index d0d38405..e8a15437 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -244,6 +244,9 @@ class CopilotTokenTracker implements vscode.Disposable { // These are reference prices for cost estimation purposes only private modelPricing: { [key: string]: ModelPricing } = modelPricingData.pricing as { [key: string]: ModelPricing }; + // GitHub authentication session + private githubSession: vscode.AuthenticationSession | undefined; + // Tool name mapping - loaded from toolNames.json for friendly display names private toolNameMap: { [key: string]: string } = toolNamesData as { [key: string]: string }; @@ -995,6 +998,71 @@ class CopilotTokenTracker implements vscode.Disposable { this.statusBarItem.text = this._devBranch ? `${text} [${this._devBranch}]` : text; } + /** + * Authenticate with GitHub using VS Code's authentication API. + */ + public async authenticateWithGitHub(): Promise { + try { + this.log('Attempting GitHub authentication...'); + const session = await vscode.authentication.getSession( + 'github', + ['read:user'], + { createIfNone: true } + ); + if (session) { + this.githubSession = session; + this.log(`✅ Successfully authenticated as ${session.account.label}`); + vscode.window.showInformationMessage(`GitHub authentication successful! Logged in as ${session.account.label}`); + await this.context.globalState.update('github.authenticated', true); + await this.context.globalState.update('github.username', session.account.label); + } + } catch (error) { + this.error('GitHub authentication failed:', error); + vscode.window.showErrorMessage('Failed to authenticate with GitHub. Please try again.'); + } + } + + /** + * Sign out from GitHub. + */ + public async signOutFromGitHub(): Promise { + try { + this.log('Signing out from GitHub...'); + this.githubSession = undefined; + await this.context.globalState.update('github.authenticated', false); + await this.context.globalState.update('github.username', undefined); + this.log('✅ Successfully signed out from GitHub'); + vscode.window.showInformationMessage('Signed out from GitHub successfully.'); + } catch (error) { + this.error('Failed to sign out from GitHub:', error); + vscode.window.showErrorMessage('Failed to sign out from GitHub.'); + } + } + + /** + * Get the current GitHub authentication status. + */ + public getGitHubAuthStatus(): { authenticated: boolean; username?: string } { + const authenticated = this.context.globalState.get('github.authenticated', false); + const username = this.context.globalState.get('github.username'); + return { authenticated, username }; + } + + /** + * Check if the user is authenticated with GitHub. + */ + public isGitHubAuthenticated(): boolean { + return this.githubSession !== undefined || + this.context.globalState.get('github.authenticated', false); + } + + /** + * Get the current GitHub session (if authenticated). + */ + public getGitHubSession(): vscode.AuthenticationSession | undefined { + return this.githubSession; + } + public async updateTokenStats(silent: boolean = false): Promise { try { this.log('Updating token stats...'); @@ -7908,6 +7976,24 @@ export function activate(context: vscode.ExtensionContext) { }, ); + // Register the GitHub authentication command + const authenticateGitHubCommand = vscode.commands.registerCommand( + "copilot-token-tracker.authenticateGitHub", + async () => { + tokenTracker.log("GitHub authentication command called"); + await tokenTracker.authenticateWithGitHub(); + }, + ); + + // Register the GitHub sign out command + const signOutGitHubCommand = vscode.commands.registerCommand( + "copilot-token-tracker.signOutGitHub", + async () => { + tokenTracker.log("GitHub sign out command called"); + await tokenTracker.signOutFromGitHub(); + }, + ); + // Add to subscriptions for proper cleanup context.subscriptions.push( refreshCommand, @@ -7921,6 +8007,8 @@ export function activate(context: vscode.ExtensionContext) { showEnvironmentalCommand, generateDiagnosticReportCommand, clearCacheCommand, + authenticateGitHubCommand, + signOutGitHubCommand, tokenTracker, ); From 4099885e97d4655bfefeb2983c147f37a39d2492 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:21:27 +0000 Subject: [PATCH 03/22] Add GitHub authentication status to diagnostics panel Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- vscode-extension/src/extension.ts | 4 + .../src/webview/diagnostics/main.ts | 85 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index e8a15437..2922d446 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -7208,6 +7208,9 @@ ${hashtag}`; `Backend storage info retrieved: enabled=${backendStorageInfo.enabled}, configured=${backendStorageInfo.isConfigured}`, ); + // Get GitHub authentication status + const githubAuthStatus = this.getGitHubAuthStatus(); + // Check if panel is still open before updating if (!this.isPanelOpen(panel)) { this.log("Diagnostic panel closed during data load, aborting update"); @@ -7225,6 +7228,7 @@ ${hashtag}`; sessionFolders, candidatePaths, backendStorageInfo, + githubAuth: githubAuthStatus, }); this.log("✅ Diagnostic data loaded and sent to webview"); diff --git a/vscode-extension/src/webview/diagnostics/main.ts b/vscode-extension/src/webview/diagnostics/main.ts index 85f98956..74460ad3 100644 --- a/vscode-extension/src/webview/diagnostics/main.ts +++ b/vscode-extension/src/webview/diagnostics/main.ts @@ -65,6 +65,11 @@ type GlobalStateCounters = { unknownMcpDismissedVersion: string; }; +type GitHubAuthStatus = { + authenticated: boolean; + username?: string; +}; + type DiagnosticsData = { report: string; sessionFiles: { file: string; size: number; modified: string }[]; @@ -74,6 +79,7 @@ type DiagnosticsData = { backendConfigured?: boolean; isDebugMode?: boolean; globalStateCounters?: GlobalStateCounters; + githubAuth?: GitHubAuthStatus; }; type DiagnosticsViewState = { @@ -711,6 +717,70 @@ function renderDebugTab(counters: GlobalStateCounters | undefined): string { `; } +function renderGitHubAuthPanel(githubAuth: GitHubAuthStatus | undefined): string { + const authenticated = githubAuth?.authenticated || false; + const username = githubAuth?.username || ''; + + const statusColor = authenticated ? '#2d6a4f' : '#666'; + const statusIcon = authenticated ? '✅' : '⚪'; + const statusText = authenticated ? 'Authenticated' : 'Not Authenticated'; + + return ` +
+
🔑 GitHub Authentication
+
+ Authenticate with GitHub to unlock additional features in future releases. +
+
+ +
+
+
${statusIcon} Status
+
${statusText}
+
+ ${authenticated ? ` +
+
👤 Logged in as
+
${escapeHtml(username)}
+
+ ` : ''} +
+ +${authenticated ? ` +
+

+ You are currently authenticated with GitHub. This enables future features such as: +

+
    +
  • Repository-specific usage tracking
  • +
  • Team collaboration features
  • +
  • Advanced analytics and insights
  • +
+
+` : ` +
+

+ Sign in with your GitHub account to unlock future features. This uses VS Code's built-in authentication. +

+
+`} + +
+ ${authenticated ? ` + + ` : ` + + `} +
+ `; +} + function renderBackendStoragePanel( backendInfo: BackendStorageInfo | undefined, ): string { @@ -991,6 +1061,7 @@ function renderLayout(data: DiagnosticsData): void { + ${data.isDebugMode ? '' : ''} @@ -1082,6 +1153,10 @@ function renderLayout(data: DiagnosticsData): void {
${renderBackendStoragePanel(data.backendStorageInfo)}
+ +
+ ${renderGitHubAuthPanel(data.githubAuth)} +
⚙️ Display Settings
@@ -1413,6 +1488,16 @@ function renderLayout(data: DiagnosticsData): void { } }); }); + + document.getElementById('btn-authenticate-github')?.addEventListener('click', () => { + console.log('[DEBUG] Authenticate GitHub button clicked'); + vscode.postMessage({ command: 'authenticateGitHub' }); + }); + + document.getElementById('btn-sign-out-github')?.addEventListener('click', () => { + console.log('[DEBUG] Sign out GitHub button clicked'); + vscode.postMessage({ command: 'signOutGitHub' }); + }); } // Helper function to activate a tab by its ID From ea97b0a5744f262350a61a724784d7405752807b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:22:21 +0000 Subject: [PATCH 04/22] Add documentation for GitHub authentication feature Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- docs/vscode-extension/README.md | 13 +++++++++++++ vscode-extension/CHANGELOG.md | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/docs/vscode-extension/README.md b/docs/vscode-extension/README.md index 406430ef..0c6df00f 100644 --- a/docs/vscode-extension/README.md +++ b/docs/vscode-extension/README.md @@ -25,6 +25,19 @@ Search for **"AI Engineering Fluency"** in the VS Code Extensions panel, or inst - **Smart Estimation**: Uses character-based analysis with model-specific ratios for token estimation - **Intelligent Caching**: Caches processed session files to speed up subsequent updates - **Diagnostic Reporting**: Generate comprehensive diagnostic reports to help troubleshoot issues +- **GitHub Authentication**: Authenticate with your GitHub account to unlock future features + +### GitHub Authentication (Opt-in) + +- **Opt-in Authentication**: Sign in with your configured GitHub account in VS Code +- **Built-in VS Code Integration**: Uses VS Code's native authentication provider for GitHub +- **Secure Storage**: Authentication state is securely stored in VS Code's global state +- **Future Features**: Foundation for upcoming GitHub-specific features such as: + - Repository-specific usage tracking + - Team collaboration features + - Advanced analytics and insights + +To authenticate, open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and search for "AI Engineering Fluency: Authenticate with GitHub", or access it through the Diagnostic Report's **GitHub Auth** tab. ### Cloud Backend (Opt-in) diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md index f94ebb22..3f0c7669 100644 --- a/vscode-extension/CHANGELOG.md +++ b/vscode-extension/CHANGELOG.md @@ -6,6 +6,12 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] +### ✨ Features & Improvements +- Added GitHub authentication support using VS Code's built-in authentication provider +- New commands: "Authenticate with GitHub" and "Sign Out from GitHub" +- GitHub Auth tab in Diagnostic Report panel showing authentication status +- Foundation for future GitHub-specific features (repository tracking, team collaboration, advanced analytics) + ## [0.0.27] - 2026-04-07 ### ✨ Features & Improvements From aaddda31b3d61683cd570b68192cce4ba3f95c11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:23:41 +0000 Subject: [PATCH 05/22] Improve GitHub authentication session management and synchronization Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com> --- vscode-extension/src/extension.ts | 45 +++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 2922d446..649e5b3a 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -847,6 +847,9 @@ class CopilotTokenTracker implements vscode.Disposable { // Load persisted cache from storage this.cacheManager.loadCacheFromStorage(); + // Restore GitHub authentication session if previously authenticated + this.restoreGitHubSession(); + // Check GitHub Copilot extension status this.sessionDiscovery.checkCopilotExtension(); @@ -1052,8 +1055,14 @@ class CopilotTokenTracker implements vscode.Disposable { * Check if the user is authenticated with GitHub. */ public isGitHubAuthenticated(): boolean { - return this.githubSession !== undefined || - this.context.globalState.get('github.authenticated', false); + // Primary check: in-memory session + if (this.githubSession !== undefined) { + return true; + } + // Fallback: check persisted state (session may not be restored yet) + // Note: This may be true even if the session is expired + // The restoreGitHubSession method will reconcile this on startup + return this.context.globalState.get('github.authenticated', false); } /** @@ -1063,6 +1072,38 @@ class CopilotTokenTracker implements vscode.Disposable { return this.githubSession; } + /** + * Restore GitHub authentication session on extension startup. + * Attempts to silently retrieve an existing session if user was previously authenticated. + */ + private async restoreGitHubSession(): Promise { + try { + const wasAuthenticated = this.context.globalState.get('github.authenticated', false); + if (wasAuthenticated) { + this.log('Attempting to restore GitHub authentication session...'); + // Try to get the existing session without prompting the user + // createIfNone: false ensures we don't prompt for authentication + const session = await vscode.authentication.getSession('github', ['read:user'], { createIfNone: false }); + if (session) { + this.githubSession = session; + this.log(`✅ GitHub session restored for ${session.account.label}`); + // Update the stored username in case it changed + await this.context.globalState.update('github.username', session.account.label); + } else { + // Session doesn't exist anymore - clear the authenticated state + this.log('GitHub session not found - clearing authenticated state'); + await this.context.globalState.update('github.authenticated', false); + await this.context.globalState.update('github.username', undefined); + } + } + } catch (error) { + this.warn('Failed to restore GitHub session: ' + String(error)); + // Clear authentication state on error + await this.context.globalState.update('github.authenticated', false); + await this.context.globalState.update('github.username', undefined); + } + } + public async updateTokenStats(silent: boolean = false): Promise { try { this.log('Updating token stats...'); From 169cd4c4077b154d88e6f2fdccce83812514cbf0 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 20:04:07 +0200 Subject: [PATCH 06/22] fix: wire up GitHub auth button handlers in diagnostics webview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add case 'authenticateGitHub' and 'signOutGitHub' to the diagnostics panel onDidReceiveMessage handler in extension.ts — previously these messages were silently dropped because only VS Code commands were registered, not webview message handlers. - Move GitHub button event listeners out of setupStorageLinkHandlers (which only runs when session folders load) into a dedicated setupGitHubAuthHandlers() function called at initialization. - Add 'githubAuthUpdated' message handler so the GitHub Auth tab re-renders immediately after login/logout without a full reload. - Remove debug console.log statements from button handlers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/extension.ts | 22 +++++++++++++++++++ .../src/webview/diagnostics/main.ts | 12 ++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 649e5b3a..43be4aaa 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -7125,6 +7125,28 @@ ${hashtag}`; }); } break; + case "authenticateGitHub": + await this.dispatch('authenticateGitHub:diagnostics', async () => { + await this.authenticateWithGitHub(); + if (this.diagnosticsPanel) { + this.diagnosticsPanel.webview.postMessage({ + command: 'githubAuthUpdated', + githubAuth: this.getGitHubAuthStatus(), + }); + } + }); + break; + case "signOutGitHub": + await this.dispatch('signOutGitHub:diagnostics', async () => { + await this.signOutFromGitHub(); + if (this.diagnosticsPanel) { + this.diagnosticsPanel.webview.postMessage({ + command: 'githubAuthUpdated', + githubAuth: this.getGitHubAuthStatus(), + }); + } + }); + break; } }); diff --git a/vscode-extension/src/webview/diagnostics/main.ts b/vscode-extension/src/webview/diagnostics/main.ts index 74460ad3..5ef50781 100644 --- a/vscode-extension/src/webview/diagnostics/main.ts +++ b/vscode-extension/src/webview/diagnostics/main.ts @@ -1379,6 +1379,13 @@ function renderLayout(data: DiagnosticsData): void { } // Diagnostic data loaded successfully - no console needed as this is normal operation + } else if (message.command === "githubAuthUpdated") { + // Update GitHub Auth tab with new authentication status + const githubTabContent = document.getElementById("tab-github"); + if (githubTabContent) { + githubTabContent.innerHTML = renderGitHubAuthPanel(message.githubAuth); + setupGitHubAuthHandlers(); + } } else if (message.command === "diagnosticDataError") { // Show error message console.error("Error loading diagnostic data:", message.error); @@ -1488,14 +1495,14 @@ function renderLayout(data: DiagnosticsData): void { } }); }); + } + function setupGitHubAuthHandlers(): void { document.getElementById('btn-authenticate-github')?.addEventListener('click', () => { - console.log('[DEBUG] Authenticate GitHub button clicked'); vscode.postMessage({ command: 'authenticateGitHub' }); }); document.getElementById('btn-sign-out-github')?.addEventListener('click', () => { - console.log('[DEBUG] Sign out GitHub button clicked'); vscode.postMessage({ command: 'signOutGitHub' }); }); } @@ -1835,6 +1842,7 @@ function renderLayout(data: DiagnosticsData): void { setupBackendButtonHandlers(); setupFileLinks(); setupStorageLinkHandlers(); + setupGitHubAuthHandlers(); // Restore active tab from saved state, with fallback to default const savedState = vscode.getState(); From 51372866da4e5ba87b651ac4935891ff715c3bb6 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 20:24:50 +0200 Subject: [PATCH 07/22] feat: add Repository PRs tab to Usage Analysis view - Add RepoPrDetail/RepoPrInfo/RepoPrStatsResult types - Add detectAiType(), discoverGitHubRepos(), fetchRepoPrsPage(), fetchRepoPrs(), loadRepoPrStats() methods in extension.ts - Wire case 'loadRepoPrStats' in analysisPanel message handler - Add 4th tab button to Usage Analysis tab bar - Add tab-panel-repos div with lazy-load trigger on first tab click - Add renderReposPrContent() and updateReposPrPanel() in webview - Handle repoPrStatsLoaded and repoPrStatsProgress messages - Paginate GitHub PR API (up to 5 pages/500 PRs per repo) - Detect Copilot/Claude/OpenAI bots as PR authors or review requestees - Show auth-required message when user is not signed in - Add https import at top-level to replace inline require() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/extension.ts | 228 +++++++++++++++++++++ vscode-extension/src/webview/usage/main.ts | 153 ++++++++++++++ 2 files changed, 381 insertions(+) diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 43be4aaa..c72688fb 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +import * as https from 'https'; import * as childProcess from 'child_process'; import tokenEstimatorsData from './tokenEstimators.json'; import modelPricingData from './modelPricing.json'; @@ -139,6 +140,31 @@ type LocalViewRegressionCase = { open: () => Promise; }; +type RepoPrDetail = { + number: number; + title: string; + url: string; + aiType: 'copilot' | 'claude' | 'openai' | 'other-ai'; + role: 'author' | 'reviewer-requested'; +}; + +type RepoPrInfo = { + owner: string; + repo: string; + repoUrl: string; + totalPrs: number; + aiAuthoredPrs: number; + aiReviewRequestedPrs: number; + aiDetails: RepoPrDetail[]; + error?: string; +}; + +type RepoPrStatsResult = { + repos: RepoPrInfo[]; + authenticated: boolean; + since: string; // ISO date string +}; + class CopilotTokenTracker implements vscode.Disposable { // Cache version - increment this when making changes that require cache invalidation private static readonly CACHE_VERSION = 36; // Add first-user-message fallback title for untitled Copilot CLI sessions @@ -247,6 +273,9 @@ class CopilotTokenTracker implements vscode.Disposable { // GitHub authentication session private githubSession: vscode.AuthenticationSession | undefined; + // Cached PR stats result for the repos tab + private _lastRepoPrStats?: RepoPrStatsResult; + // Tool name mapping - loaded from toolNames.json for friendly display names private toolNameMap: { [key: string]: string } = toolNamesData as { [key: string]: string }; @@ -1072,6 +1101,202 @@ class CopilotTokenTracker implements vscode.Disposable { return this.githubSession; } + /** Detect which AI system a GitHub login belongs to, or null if not an AI bot. */ + private detectAiType(login: string): RepoPrDetail['aiType'] | null { + const l = login.toLowerCase(); + if (l.includes('copilot')) { return 'copilot'; } + if (l.includes('claude') || l.includes('anthropic')) { return 'claude'; } + if (l.includes('openai') || l.includes('codex')) { return 'openai'; } + return null; + } + + /** + * Discover GitHub repos from known session workspace folders. + * Deduplicates by owner/repo so each GitHub repo is only fetched once. + */ + private async discoverGitHubRepos(): Promise<{ owner: string; repo: string }[]> { + const workspacePaths: string[] = []; + + const matrix = this._lastCustomizationMatrix; + if (matrix && matrix.workspaces.length > 0) { + for (const ws of matrix.workspaces) { + if (!ws.workspacePath.startsWith('(); + const repos: { owner: string; repo: string }[] = []; + for (const workspacePath of workspacePaths) { + try { + const remote = childProcess.execSync('git remote get-url origin', { + cwd: workspacePath, + encoding: 'utf8', + timeout: 3000, + stdio: ['pipe', 'pipe', 'pipe'], + }).trim(); + // Only process github.com remotes + const match = remote.match(/github\.com[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/i); + if (!match) { continue; } + const key = `${match[1]}/${match[2]}`.toLowerCase(); + if (seen.has(key)) { continue; } + seen.add(key); + repos.push({ owner: match[1], repo: match[2] }); + } catch { + // Not a git repo or no remote — skip + } + } + return repos; + } + + /** Fetch a single page of PRs from GitHub REST API. */ + private fetchRepoPrsPage( + owner: string, + repo: string, + token: string, + page: number, + ): Promise<{ prs: any[]; statusCode?: number; error?: string }> { + return new Promise((resolve) => { + const req = https.request( + { + hostname: 'api.github.com', + path: `/repos/${owner}/${repo}/pulls?state=all&per_page=100&sort=created&direction=desc&page=${page}`, + headers: { + Authorization: `Bearer ${token}`, + 'User-Agent': 'copilot-token-tracker', + Accept: 'application/vnd.github.v3+json', + }, + }, + (res) => { + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => { + try { + const parsed = JSON.parse(data); + if (!Array.isArray(parsed)) { + resolve({ prs: [], statusCode: res.statusCode, error: parsed.message ?? 'Unexpected API response' }); + } else { + resolve({ prs: parsed, statusCode: res.statusCode }); + } + } catch (e) { + resolve({ prs: [], statusCode: res.statusCode, error: String(e) }); + } + }); + }, + ); + req.on('error', (e) => resolve({ prs: [], error: e.message })); + req.end(); + }); + } + + /** Fetch all PRs from the last 30 days for a repo, paginating as needed. */ + private async fetchRepoPrs( + owner: string, + repo: string, + token: string, + since: Date, + ): Promise<{ prs: any[]; error?: string }> { + const allPrs: any[] = []; + const MAX_PAGES = 5; // Cap at 500 PRs per repo + for (let page = 1; page <= MAX_PAGES; page++) { + const { prs, statusCode, error } = await this.fetchRepoPrsPage(owner, repo, token, page); + if (error) { + const msg = statusCode === 404 + ? 'Repo not found or not accessible with current token' + : statusCode === 403 + ? 'Access denied (private repo requires additional permissions)' + : error; + return { prs: allPrs, error: msg }; + } + if (prs.length === 0) { break; } + for (const pr of prs) { + if (new Date(pr.created_at) >= since) { + allPrs.push(pr); + } + } + // Stop paginating when the oldest PR on this page is before our window + const oldest = prs[prs.length - 1]; + if (new Date(oldest.created_at) < since || prs.length < 100) { + break; + } + } + return { prs: allPrs }; + } + + /** Load PR stats for all discovered GitHub repos and send results to the analysis panel. */ + private async loadRepoPrStats(): Promise { + if (!this.analysisPanel) { return; } + + const since = new Date(); + since.setDate(since.getDate() - 30); + + // Require GitHub auth — read:user gives 5000 req/hr on public repos + const session = await vscode.authentication.getSession('github', ['read:user'], { createIfNone: false }); + if (!session) { + const result: RepoPrStatsResult = { repos: [], authenticated: false, since: since.toISOString() }; + this._lastRepoPrStats = result; + this.analysisPanel.webview.postMessage({ command: 'repoPrStatsLoaded', data: result }); + return; + } + + const repos = await this.discoverGitHubRepos(); + this.analysisPanel.webview.postMessage({ command: 'repoPrStatsProgress', total: repos.length, done: 0 }); + + const results: RepoPrInfo[] = []; + for (let i = 0; i < repos.length; i++) { + const { owner, repo } = repos[i]; + const { prs, error } = await this.fetchRepoPrs(owner, repo, session.accessToken, since); + + let totalPrs = 0; + let aiAuthoredPrs = 0; + let aiReviewRequestedPrs = 0; + const aiDetails: RepoPrDetail[] = []; + + if (!error) { + totalPrs = prs.length; + for (const pr of prs) { + const authorAi = this.detectAiType(pr.user?.login ?? ''); + if (authorAi) { + aiAuthoredPrs++; + aiDetails.push({ number: pr.number, title: pr.title, url: pr.html_url, aiType: authorAi, role: 'author' }); + } + for (const reviewer of (pr.requested_reviewers ?? [])) { + const reviewerAi = this.detectAiType(reviewer.login ?? ''); + if (reviewerAi) { + aiReviewRequestedPrs++; + aiDetails.push({ number: pr.number, title: pr.title, url: pr.html_url, aiType: reviewerAi, role: 'reviewer-requested' }); + } + } + } + } + + results.push({ + owner, + repo, + repoUrl: `https://github.com/${owner}/${repo}`, + totalPrs, + aiAuthoredPrs, + aiReviewRequestedPrs, + aiDetails, + error, + }); + + this.analysisPanel.webview.postMessage({ command: 'repoPrStatsProgress', total: repos.length, done: i + 1 }); + } + + const result: RepoPrStatsResult = { repos: results, authenticated: true, since: since.toISOString() }; + this._lastRepoPrStats = result; + this.analysisPanel.webview.postMessage({ command: 'repoPrStatsLoaded', data: result }); + } + /** * Restore GitHub authentication session on extension startup. * Attempts to silently retrieve an existing session if user was previously authenticated. @@ -4522,6 +4747,9 @@ class CopilotTokenTracker implements vscode.Disposable { } break; } + case 'loadRepoPrStats': + await this.dispatch('loadRepoPrStats', () => this.loadRepoPrStats()); + break; } }); diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index f0a8f4b1..8ff5345e 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -117,6 +117,35 @@ let isBatchAnalysisInProgress = false; let currentWorkspacePaths: string[] = []; let activeTab = 'activity'; +// State for the Repository PRs tab +let repoPrStatsLoaded = false; +let repoPrStatsData: RepoPrStatsResult | null = null; + +type RepoPrDetail = { + number: number; + title: string; + url: string; + aiType: 'copilot' | 'claude' | 'openai' | 'other-ai'; + role: 'author' | 'reviewer-requested'; +}; + +type RepoPrInfo = { + owner: string; + repo: string; + repoUrl: string; + totalPrs: number; + aiAuthoredPrs: number; + aiReviewRequestedPrs: number; + aiDetails: RepoPrDetail[]; + error?: string; +}; + +type RepoPrStatsResult = { + repos: RepoPrInfo[]; + authenticated: boolean; + since: string; +}; + function escapeHtml(text: string): string { return text .replace(/&/g, '&') @@ -398,10 +427,98 @@ function setupTabs(): void { }); const activePanel = document.getElementById(`tab-panel-${tab}`); if (activePanel) { activePanel.style.display = 'block'; } + // Lazy-load repo PR stats on first visit to the tab + if (tab === 'repos' && !repoPrStatsLoaded) { + repoPrStatsLoaded = true; + vscode.postMessage({ command: 'loadRepoPrStats' }); + } }); }); } +function renderReposPrContent(data: RepoPrStatsResult): string { + const sinceDate = new Date(data.since).toLocaleDateString(); + if (!data.authenticated) { + return ` +
+ 🔒 GitHub authentication required
+ Sign in with GitHub (via the Diagnostics tab) to see AI PR activity across your repositories. +
`; + } + if (data.repos.length === 0) { + return ` +
+ No GitHub repositories detected in your workspace folders. +
`; + } + + const aiLabel: Record = { + copilot: '🤖 Copilot', + claude: '🧠 Claude', + openai: '✨ OpenAI', + 'other-ai': '🤖 AI', + }; + + const rows = data.repos.map((r) => { + const repoLink = `${escapeHtml(r.owner)}/${escapeHtml(r.repo)}`; + if (r.error) { + return ` + ${repoLink} + ${escapeHtml(r.error)} + `; + } + // Collapse detail links + let detailsHtml = ''; + if (r.aiDetails.length > 0) { + const items = r.aiDetails.map(d => + `
  • #${d.number} ${escapeHtml(d.title)} — ${aiLabel[d.aiType] ?? d.aiType} (${d.role === 'author' ? 'authored' : 'review requested'})
  • ` + ).join(''); + detailsHtml = ` +
    + Show ${r.aiDetails.length} detail(s) +
      ${items}
    +
    `; + } + return ` + ${repoLink}${detailsHtml} + ${r.totalPrs} + ${r.aiAuthoredPrs} + ${r.aiReviewRequestedPrs} + `; + }).join(''); + + return ` +
    + Showing PRs created since ${sinceDate}. Reviewer requests shown for open PRs only — merged/closed PR reviews are not captured by the GitHub API. +
    + + + + + + + + + + + ${rows} + +
    RepositoryTotal PRsAI AuthoredAI Review Requested†
    +
    + † Review requests visible only on open PRs. Closed/merged PRs don't expose reviewer data in the GitHub API. +
    `; +} + +function updateReposPrPanel(data: RepoPrStatsResult): void { + const container = document.querySelector('#repos-pr-content'); + if (!container) { return; } + container.innerHTML = ` +
    🤖AI Activity in Repository PRs
    +
    PRs from the last 30 days across your known repositories — authored or reviewed by AI agents.
    + ${renderReposPrContent(data)} + `; +} + function renderLayout(stats: UsageAnalysisStats): void { const root = document.getElementById('root'); if (!root) { @@ -794,6 +911,7 @@ function renderLayout(stats: UsageAnalysisStats): void { +
    + + @@ -1211,6 +1339,31 @@ window.addEventListener('message', (event) => { } break; } + case 'repoPrStatsLoaded': { + repoPrStatsData = message.data as RepoPrStatsResult; + updateReposPrPanel(repoPrStatsData); + break; + } + case 'repoPrStatsProgress': { + const container = document.querySelector('#repos-pr-content'); + if (container) { + const done = message.done as number; + const total = message.total as number; + const pct = total > 0 ? Math.round((done / total) * 100) : 0; + const progEl = container.querySelector('.repos-pr-progress'); + if (progEl) { + progEl.textContent = `Fetching PRs… ${done}/${total} repos (${pct}%)`; + } else { + // Inject a progress indicator + const div = document.createElement('div'); + div.className = 'repos-pr-progress'; + div.style.cssText = 'margin-top:8px; font-size:12px; color:var(--text-secondary);'; + div.textContent = `Fetching PRs… ${done}/${total} repos (${pct}%)`; + container.appendChild(div); + } + } + break; + } } }); From 6ea1e2a41e6e334658f897c05763c379d467d721 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:31:48 +0200 Subject: [PATCH 08/22] style: match repo PRs table to workspace health table styling - Use customization-matrix CSS class and border-bottom: 2px header - Apply border-bottom: 1px solid var(--border-subtle) to all data cells - Use monospace font and var(--link-color) for repo names, matching customization matrix - Highlight non-zero AI counts in link color with bold weight - Update section subtitle to explain 'AI authored' means the PR author's GitHub login matches a known AI agent bot (e.g. copilot-swe-agent) - Add footer legend clarifying AI Authored detection logic and review caveat - Add title attributes to column headers with more detail on hover Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 66 +++++++++++++--------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index 8ff5345e..98847c2c 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -459,19 +459,23 @@ function renderReposPrContent(data: RepoPrStatsResult): string { 'other-ai': '🤖 AI', }; + // Cell style shared across data rows — matches the customization matrix look + const cell = 'padding: 6px 8px; border-bottom: 1px solid var(--border-subtle);'; + const cellCenter = `${cell} text-align: center;`; + const rows = data.repos.map((r) => { - const repoLink = `${escapeHtml(r.owner)}/${escapeHtml(r.repo)}`; + const repoLink = `${escapeHtml(r.owner)}/${escapeHtml(r.repo)}`; if (r.error) { return ` - ${repoLink} - ${escapeHtml(r.error)} + ${repoLink} + ${escapeHtml(r.error)} `; } - // Collapse detail links + // Collapsible detail list let detailsHtml = ''; if (r.aiDetails.length > 0) { const items = r.aiDetails.map(d => - `
  • #${d.number} ${escapeHtml(d.title)} — ${aiLabel[d.aiType] ?? d.aiType} (${d.role === 'author' ? 'authored' : 'review requested'})
  • ` + `
  • #${d.number} ${escapeHtml(d.title)} — ${aiLabel[d.aiType] ?? d.aiType} (${d.role === 'author' ? 'authored' : 'review requested'})
  • ` ).join(''); detailsHtml = `
    @@ -480,32 +484,36 @@ function renderReposPrContent(data: RepoPrStatsResult): string {
    `; } return ` - ${repoLink}${detailsHtml} - ${r.totalPrs} - ${r.aiAuthoredPrs} - ${r.aiReviewRequestedPrs} + ${repoLink}${detailsHtml} + ${r.totalPrs} + ${r.aiAuthoredPrs > 0 ? `${r.aiAuthoredPrs}` : '0'} + ${r.aiReviewRequestedPrs > 0 ? `${r.aiReviewRequestedPrs}` : '0'} `; }).join(''); return ` -
    - Showing PRs created since ${sinceDate}. Reviewer requests shown for open PRs only — merged/closed PR reviews are not captured by the GitHub API. +
    + Showing PRs created since ${sinceDate}. + Reviewer requests are only visible for open PRs — the GitHub API clears this field after a PR is merged or closed.
    - - - - - - - - - - - ${rows} - -
    RepositoryTotal PRsAI AuthoredAI Review Requested†
    -
    - † Review requests visible only on open PRs. Closed/merged PRs don't expose reviewer data in the GitHub API. +
    + + + + + + + + + + + ${rows} + +
    📂 RepositoryPRs🤖 AI Authored👁 AI Review Req†
    +
    +
    + † AI Review Requested counts are for open PRs only. GitHub removes reviewer data after a PR is merged or closed.
    + 🤖 AI Authored = PR author's GitHub login contains "copilot", "claude", "openai", or "codex".
    `; } @@ -514,7 +522,11 @@ function updateReposPrPanel(data: RepoPrStatsResult): void { if (!container) { return; } container.innerHTML = `
    🤖AI Activity in Repository PRs
    -
    PRs from the last 30 days across your known repositories — authored or reviewed by AI agents.
    +
    + PRs from the last 30 days across your known repositories, showing how many were authored by AI agents + (i.e. opened by a bot account like copilot-swe-agent or claude-code-action) + or had an AI agent requested as a reviewer. +
    ${renderReposPrContent(data)} `; } From 1e7eb13a12fc3f51f9b5b5ebbd666a67ce130295 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:33:34 +0200 Subject: [PATCH 09/22] fix: restore repo PRs tab data after scheduled stats refresh renderLayout() replaces the entire #root DOM every 5 minutes via the updateStats message, resetting the repos tab back to the loading placeholder. Mirror the same pattern used for repo hygiene panels: after renderLayout(), call updateReposPrPanel(repoPrStatsData) if cached data exists so the tab content is immediately restored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index 98847c2c..ed9b8f8e 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -1325,6 +1325,10 @@ window.addEventListener('message', (event) => { if (sanitized) { renderLayout(sanitized); renderRepositoryHygienePanels(); + // Restore repos PR tab if we already fetched data (renderLayout resets the DOM) + if (repoPrStatsData) { + updateReposPrPanel(repoPrStatsData); + } } } break; From 55e691867660407ca648b6f1da9b8fa32ac06524 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:34:46 +0200 Subject: [PATCH 10/22] fix: display 'Codex' instead of 'OpenAI' for openai-type AI PRs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index ed9b8f8e..c5d19416 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -455,7 +455,7 @@ function renderReposPrContent(data: RepoPrStatsResult): string { const aiLabel: Record = { copilot: '🤖 Copilot', claude: '🧠 Claude', - openai: '✨ OpenAI', + openai: '✨ Codex', 'other-ai': '🤖 AI', }; From 5097fa407fe2a52dbde08655634c0c56e4ebb43d Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:36:09 +0200 Subject: [PATCH 11/22] docs: add openai-code-agent to AI author examples in description Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index c5d19416..523a2fe9 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -502,7 +502,7 @@ function renderReposPrContent(data: RepoPrStatsResult): string { 📂 Repository PRs - 🤖 AI Authored + 🤖 AI Authored 👁 AI Review Req† @@ -524,7 +524,7 @@ function updateReposPrPanel(data: RepoPrStatsResult): void {
    🤖AI Activity in Repository PRs
    PRs from the last 30 days across your known repositories, showing how many were authored by AI agents - (i.e. opened by a bot account like copilot-swe-agent or claude-code-action) + (i.e. opened by a bot account like copilot-swe-agent, claude-code-action, or openai-code-agent) or had an AI agent requested as a reviewer.
    ${renderReposPrContent(data)} From a99223c3d09866096481c85dd2a2c6aec87b0821 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:40:15 +0200 Subject: [PATCH 12/22] rename: 'AI Authored' -> 'Cloud Agent Authored' in repo PRs tab Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index 523a2fe9..a931f27d 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -502,7 +502,7 @@ function renderReposPrContent(data: RepoPrStatsResult): string { 📂 Repository PRs - 🤖 AI Authored + 🤖 Cloud Agent Authored 👁 AI Review Req† @@ -513,7 +513,7 @@ function renderReposPrContent(data: RepoPrStatsResult): string {
    † AI Review Requested counts are for open PRs only. GitHub removes reviewer data after a PR is merged or closed.
    - 🤖 AI Authored = PR author's GitHub login contains "copilot", "claude", "openai", or "codex". + 🤖 Cloud Agent Authored = PR author's GitHub login contains "copilot", "claude", "openai", or "codex".
    `; } @@ -523,7 +523,7 @@ function updateReposPrPanel(data: RepoPrStatsResult): void { container.innerHTML = `
    🤖AI Activity in Repository PRs
    - PRs from the last 30 days across your known repositories, showing how many were authored by AI agents + PRs from the last 30 days across your known repositories, showing how many were authored by cloud agents (i.e. opened by a bot account like copilot-swe-agent, claude-code-action, or openai-code-agent) or had an AI agent requested as a reviewer.
    From bb3ad24fce9c297fe1509583d2476e901e95d6f4 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:40:44 +0200 Subject: [PATCH 13/22] rename: 'AI Review Req' -> 'Copilot Review Agent requested' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index a931f27d..f37976c0 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -503,7 +503,7 @@ function renderReposPrContent(data: RepoPrStatsResult): string { 📂 Repository PRs 🤖 Cloud Agent Authored - 👁 AI Review Req† + 👁 Copilot Review Agent requested† @@ -512,7 +512,7 @@ function renderReposPrContent(data: RepoPrStatsResult): string {
    - † AI Review Requested counts are for open PRs only. GitHub removes reviewer data after a PR is merged or closed.
    + † Copilot Review Agent requested counts are for open PRs only. GitHub removes reviewer data after a PR is merged or closed.
    🤖 Cloud Agent Authored = PR author's GitHub login contains "copilot", "claude", "openai", or "codex".
    `; } From 3ca91db1efa4b699c590b0c73ada805badd6c0a8 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:42:46 +0200 Subject: [PATCH 14/22] style: use default text color for count columns in repo PRs table Numbers are not clickable links so should not appear in blue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index f37976c0..c40521b8 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -485,9 +485,9 @@ function renderReposPrContent(data: RepoPrStatsResult): string { } return ` ${repoLink}${detailsHtml} - ${r.totalPrs} - ${r.aiAuthoredPrs > 0 ? `${r.aiAuthoredPrs}` : '0'} - ${r.aiReviewRequestedPrs > 0 ? `${r.aiReviewRequestedPrs}` : '0'} + ${r.totalPrs} + ${r.aiAuthoredPrs > 0 ? `${r.aiAuthoredPrs}` : '0'} + ${r.aiReviewRequestedPrs > 0 ? `${r.aiReviewRequestedPrs}` : '0'} `; }).join(''); From 9cc6242078ba31510bc03c5537acce38427371c9 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:47:37 +0200 Subject: [PATCH 15/22] fix: sync auth state when VS Code already has a GitHub session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadRepoPrStats() calls vscode.authentication.getSession directly, which silently returns any active VS Code GitHub session (e.g. from Copilot itself) even if our extension never tracked it. This caused Diagnostics to show 'not authenticated' while PRs were loading fine. Fix: after a successful getSession, if this.githubSession is unset, sync it and update globalState so Diagnostics reflects the real state. Also fix the visual glitch where 'Loading… (sign in)' and 'Fetching PRs…' both appeared simultaneously: on the first progress message, strip the static placeholder children before appending the progress element. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/extension.ts | 9 +++++++++ vscode-extension/src/webview/usage/main.ts | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index c72688fb..052f9ba8 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -1247,6 +1247,15 @@ class CopilotTokenTracker implements vscode.Disposable { return; } + // Sync our tracked auth state if VS Code already has a session we weren't aware of + // (e.g. from GitHub Copilot or another extension that authenticated earlier) + if (!this.githubSession) { + this.githubSession = session; + await this.context.globalState.update('github.authenticated', true); + await this.context.globalState.update('github.username', session.account.label); + this.log(`✅ GitHub session synced from existing VS Code auth: ${session.account.label}`); + } + const repos = await this.discoverGitHubRepos(); this.analysisPanel.webview.postMessage({ command: 'repoPrStatsProgress', total: repos.length, done: 0 }); diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index c40521b8..f909762a 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -1370,7 +1370,13 @@ window.addEventListener('message', (event) => { if (progEl) { progEl.textContent = `Fetching PRs… ${done}/${total} repos (${pct}%)`; } else { - // Inject a progress indicator + // First progress update — strip the static placeholder (keep only title/subtitle) + Array.from(container.children).forEach(child => { + const el = child as HTMLElement; + if (!el.classList.contains('section-title') && !el.classList.contains('section-subtitle')) { + el.remove(); + } + }); const div = document.createElement('div'); div.className = 'repos-pr-progress'; div.style.cssText = 'margin-top:8px; font-size:12px; color:var(--text-secondary);'; From d004051af8ee3d59c389fce011bd8fbf1ea20363 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:48:03 +0200 Subject: [PATCH 16/22] docs: update footer legend to list actual bot names instead of login substrings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index f909762a..524d94ba 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -513,7 +513,7 @@ function renderReposPrContent(data: RepoPrStatsResult): string {
    † Copilot Review Agent requested counts are for open PRs only. GitHub removes reviewer data after a PR is merged or closed.
    - 🤖 Cloud Agent Authored = PR author's GitHub login contains "copilot", "claude", "openai", or "codex". + 🤖 Cloud Agent Authored = PR author's GitHub login matches a known cloud agent (e.g. copilot-swe-agent, claude-code-action, openai-code-agent).
    `; } From 7d6347f3677641c55ce9ce773132655bcd5e2ed6 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:51:37 +0200 Subject: [PATCH 17/22] fix: always attempt silent getSession on startup regardless of stored flag Previously restoreGitHubSession() only called getSession when globalState 'github.authenticated' was true. This meant users who had a GitHub session from Copilot (or another extension) but had never explicitly clicked Authenticate in our extension would always appear as 'not authenticated' in Diagnostics, even though PR data loaded fine. Fix: unconditionally call getSession with createIfNone:false on startup. If a session exists, sync githubSession and update globalState. This ensures Diagnostics and the Repository PRs tab are always in sync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/extension.ts | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 052f9ba8..58eed79d 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -1308,23 +1308,23 @@ class CopilotTokenTracker implements vscode.Disposable { /** * Restore GitHub authentication session on extension startup. - * Attempts to silently retrieve an existing session if user was previously authenticated. + * Always attempts a silent getSession so that a pre-existing VS Code GitHub + * session (e.g. from GitHub Copilot) is picked up automatically. */ private async restoreGitHubSession(): Promise { try { - const wasAuthenticated = this.context.globalState.get('github.authenticated', false); - if (wasAuthenticated) { - this.log('Attempting to restore GitHub authentication session...'); - // Try to get the existing session without prompting the user - // createIfNone: false ensures we don't prompt for authentication - const session = await vscode.authentication.getSession('github', ['read:user'], { createIfNone: false }); - if (session) { - this.githubSession = session; - this.log(`✅ GitHub session restored for ${session.account.label}`); - // Update the stored username in case it changed - await this.context.globalState.update('github.username', session.account.label); - } else { - // Session doesn't exist anymore - clear the authenticated state + // Always try silently — never prompt. This picks up sessions from Copilot + // or other extensions that already authenticated the user with GitHub. + const session = await vscode.authentication.getSession('github', ['read:user'], { createIfNone: false }); + if (session) { + this.githubSession = session; + this.log(`✅ GitHub session found for ${session.account.label}`); + await this.context.globalState.update('github.authenticated', true); + await this.context.globalState.update('github.username', session.account.label); + } else { + const wasAuthenticated = this.context.globalState.get('github.authenticated', false); + if (wasAuthenticated) { + // Session was present before but is gone now — clear stored state this.log('GitHub session not found - clearing authenticated state'); await this.context.globalState.update('github.authenticated', false); await this.context.globalState.update('github.username', undefined); @@ -1332,7 +1332,6 @@ class CopilotTokenTracker implements vscode.Disposable { } } catch (error) { this.warn('Failed to restore GitHub session: ' + String(error)); - // Clear authentication state on error await this.context.globalState.update('github.authenticated', false); await this.context.globalState.update('github.username', undefined); } From 8ce149bd9acb75e2ea7f12b130f47b243c06eb52 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 21:59:50 +0200 Subject: [PATCH 18/22] fix: await session restore before reading auth state in diagnostics Three-part fix for Diagnostics showing 'not authenticated' despite VS Code having an active GitHub session: 1. Store the restoreGitHubSession() promise as _sessionRestorePromise instead of fire-and-forgetting it in the constructor. 2. Await _sessionRestorePromise in loadDiagnosticDataInBackground() before calling getGitHubAuthStatus(), eliminating the race where the panel loads before the session is set on this.githubSession. 3. getGitHubAuthStatus() now checks this.githubSession in-memory first (same as isGitHubAuthenticated()), so it never returns stale globalState when the in-memory session is already populated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/extension.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 58eed79d..171e45e5 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -272,6 +272,8 @@ class CopilotTokenTracker implements vscode.Disposable { // GitHub authentication session private githubSession: vscode.AuthenticationSession | undefined; + // Promise that resolves when the startup session restore completes + private _sessionRestorePromise: Promise | undefined; // Cached PR stats result for the repos tab private _lastRepoPrStats?: RepoPrStatsResult; @@ -877,7 +879,7 @@ class CopilotTokenTracker implements vscode.Disposable { this.cacheManager.loadCacheFromStorage(); // Restore GitHub authentication session if previously authenticated - this.restoreGitHubSession(); + this._sessionRestorePromise = this.restoreGitHubSession(); // Check GitHub Copilot extension status this.sessionDiscovery.checkCopilotExtension(); @@ -1075,6 +1077,10 @@ class CopilotTokenTracker implements vscode.Disposable { * Get the current GitHub authentication status. */ public getGitHubAuthStatus(): { authenticated: boolean; username?: string } { + // Check in-memory session first — avoids race with globalState writes on startup + if (this.githubSession) { + return { authenticated: true, username: this.githubSession.account.label }; + } const authenticated = this.context.globalState.get('github.authenticated', false); const username = this.context.globalState.get('github.username'); return { authenticated, username }; @@ -7417,6 +7423,11 @@ ${hashtag}`; try { this.log("🔄 Loading diagnostic data in background..."); + // Ensure the startup GitHub session restore has completed before reading auth state + if (this._sessionRestorePromise) { + await this._sessionRestorePromise; + } + // 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. From a76e9927e004a2a590eaa8af2f5faa0c9d4cda28 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 22:04:41 +0200 Subject: [PATCH 19/22] fix: update GitHub Auth tab when diagnosticDataLoaded message arrives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The diagnosticDataLoaded handler received githubAuth in the message payload but never used it — the GitHub Auth tab was only updated by the separate githubAuthUpdated message. So the panel always showed 'Not Authenticated' on first load even though the session was found. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/diagnostics/main.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vscode-extension/src/webview/diagnostics/main.ts b/vscode-extension/src/webview/diagnostics/main.ts index 5ef50781..e0e7838d 100644 --- a/vscode-extension/src/webview/diagnostics/main.ts +++ b/vscode-extension/src/webview/diagnostics/main.ts @@ -1379,6 +1379,15 @@ function renderLayout(data: DiagnosticsData): void { } // Diagnostic data loaded successfully - no console needed as this is normal operation + + // Update GitHub Auth tab with the auth status from the loaded data + if (message.githubAuth !== undefined) { + const githubTabContent = document.getElementById("tab-github"); + if (githubTabContent) { + githubTabContent.innerHTML = renderGitHubAuthPanel(message.githubAuth); + setupGitHubAuthHandlers(); + } + } } else if (message.command === "githubAuthUpdated") { // Update GitHub Auth tab with new authentication status const githubTabContent = document.getElementById("tab-github"); From 764cc9b5162ffa48bb4b5e3d4d64ac4065507af6 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 22:11:59 +0200 Subject: [PATCH 20/22] fix: scope GitHub sign-out to extension only, not VS Code account Sign-out now sets a persisted '_githubSignedOutByUser' flag that: - Prevents restoreGitHubSession() from auto-acquiring the VS Code session on startup - Prevents loadRepoPrStats() from re-acquiring the session via getSession() - Notifies the analysis panel to clear the Repository PRs tab immediately Authenticating again explicitly clears the flag, allowing normal operation. The webview also resets 'repoPrStatsLoaded' when unauthenticated data arrives, so clicking the Repos tab after re-authentication triggers a fresh fetch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/extension.ts | 30 ++++++++++++++++++++++ vscode-extension/src/webview/usage/main.ts | 5 ++++ 2 files changed, 35 insertions(+) diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 171e45e5..86d320f7 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -274,6 +274,8 @@ class CopilotTokenTracker implements vscode.Disposable { private githubSession: vscode.AuthenticationSession | undefined; // Promise that resolves when the startup session restore completes private _sessionRestorePromise: Promise | undefined; + /** True when the user explicitly signed out from our extension this VS Code session. Gated by globalState so it survives reloads. */ + private _githubSignedOutByUser: boolean = false; // Cached PR stats result for the repos tab private _lastRepoPrStats?: RepoPrStatsResult; @@ -1045,6 +1047,8 @@ class CopilotTokenTracker implements vscode.Disposable { ); if (session) { this.githubSession = session; + this._githubSignedOutByUser = false; + await this.context.globalState.update('github.signedOutByUser', false); this.log(`✅ Successfully authenticated as ${session.account.label}`); vscode.window.showInformationMessage(`GitHub authentication successful! Logged in as ${session.account.label}`); await this.context.globalState.update('github.authenticated', true); @@ -1063,10 +1067,21 @@ class CopilotTokenTracker implements vscode.Disposable { try { this.log('Signing out from GitHub...'); this.githubSession = undefined; + this._githubSignedOutByUser = true; await this.context.globalState.update('github.authenticated', false); await this.context.globalState.update('github.username', undefined); + await this.context.globalState.update('github.signedOutByUser', true); this.log('✅ Successfully signed out from GitHub'); vscode.window.showInformationMessage('Signed out from GitHub successfully.'); + + // Notify the analysis panel so the Repository PRs tab shows "not authenticated" + if (this.analysisPanel) { + const since = new Date(); + since.setDate(since.getDate() - 30); + const result: RepoPrStatsResult = { repos: [], authenticated: false, since: since.toISOString() }; + this._lastRepoPrStats = result; + this.analysisPanel.webview.postMessage({ command: 'repoPrStatsLoaded', data: result }); + } } catch (error) { this.error('Failed to sign out from GitHub:', error); vscode.window.showErrorMessage('Failed to sign out from GitHub.'); @@ -1244,6 +1259,14 @@ class CopilotTokenTracker implements vscode.Disposable { const since = new Date(); since.setDate(since.getDate() - 30); + // If the user explicitly signed out from our extension, don't auto-acquire the VS Code session + if (this._githubSignedOutByUser) { + const result: RepoPrStatsResult = { repos: [], authenticated: false, since: since.toISOString() }; + this._lastRepoPrStats = result; + this.analysisPanel.webview.postMessage({ command: 'repoPrStatsLoaded', data: result }); + return; + } + // Require GitHub auth — read:user gives 5000 req/hr on public repos const session = await vscode.authentication.getSession('github', ['read:user'], { createIfNone: false }); if (!session) { @@ -1319,6 +1342,13 @@ class CopilotTokenTracker implements vscode.Disposable { */ private async restoreGitHubSession(): Promise { try { + // Respect explicit sign-out — don't auto-restore until user clicks Authenticate again + this._githubSignedOutByUser = this.context.globalState.get('github.signedOutByUser', false); + if (this._githubSignedOutByUser) { + this.log('GitHub session restore skipped — user signed out explicitly'); + return; + } + // Always try silently — never prompt. This picks up sessions from Copilot // or other extensions that already authenticated the user with GitHub. const session = await vscode.authentication.getSession('github', ['read:user'], { createIfNone: false }); diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index 524d94ba..4e07a7f3 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -1357,6 +1357,11 @@ window.addEventListener('message', (event) => { } case 'repoPrStatsLoaded': { repoPrStatsData = message.data as RepoPrStatsResult; + // Reset the loaded flag when not authenticated so re-authenticating and clicking the tab + // again triggers a fresh fetch instead of showing the stale "not authenticated" placeholder. + if (!repoPrStatsData.authenticated) { + repoPrStatsLoaded = false; + } updateReposPrPanel(repoPrStatsData); break; } From 975b3fdc00ddd05260a0f31fb2db67d01ec3a9cb Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 22:30:17 +0200 Subject: [PATCH 21/22] fix: escape user-provided values in Repository PRs panel to prevent XSS Two unescaped values in renderReposPrContent() were flagged by CodeQL: 1. sinceDate (derived from data.since) was injected raw into innerHTML. Fixed: wrap toLocaleDateString() output with escapeHtml(). 2. The aiLabel fallback (aiLabel[d.aiType] ?? d.aiType) put an unescaped string into HTML when the type key was not in the map. Fixed: use escapeHtml(String(d.aiType)) as the fallback. Both values originate from the extension-to-webview postMessage payload, which CodeQL treats as an untrusted source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- vscode-extension/src/webview/usage/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vscode-extension/src/webview/usage/main.ts b/vscode-extension/src/webview/usage/main.ts index 4e07a7f3..329d5273 100644 --- a/vscode-extension/src/webview/usage/main.ts +++ b/vscode-extension/src/webview/usage/main.ts @@ -437,7 +437,7 @@ function setupTabs(): void { } function renderReposPrContent(data: RepoPrStatsResult): string { - const sinceDate = new Date(data.since).toLocaleDateString(); + const sinceDate = escapeHtml(new Date(data.since).toLocaleDateString()); if (!data.authenticated) { return `
    @@ -475,7 +475,7 @@ function renderReposPrContent(data: RepoPrStatsResult): string { let detailsHtml = ''; if (r.aiDetails.length > 0) { const items = r.aiDetails.map(d => - `
  • #${d.number} ${escapeHtml(d.title)} — ${aiLabel[d.aiType] ?? d.aiType} (${d.role === 'author' ? 'authored' : 'review requested'})
  • ` + `
  • #${d.number} ${escapeHtml(d.title)} — ${aiLabel[d.aiType] ?? escapeHtml(String(d.aiType))} (${d.role === 'author' ? 'authored' : 'review requested'})
  • ` ).join(''); detailsHtml = `
    From c6545b5140a673b88cf9463addefecd652abfd27 Mon Sep 17 00:00:00 2001 From: Rob Bos Date: Fri, 10 Apr 2026 22:38:37 +0200 Subject: [PATCH 22/22] fix: address Copilot PR review comments - Add onDidChangeSessions listener to keep github.authenticated in sync when the VS Code GitHub session changes externally (e.g. user removes the account from the Accounts menu). Skipped when user explicitly disconnected via our extension. - Add 15 s timeout to fetchRepoPrsPage() so the Usage Analysis tab never hangs indefinitely on stalled network requests. - Fix misleading 403 error: use the GitHub API's own error message instead of always printing 'private repo requires additional permissions' (GitHub also returns 403 for rate limiting). - Add target=_blank rel=noopener noreferrer to all repo/PR links in the Repository PRs panel for consistent external-link behaviour. - Update README to clarify 'stored in VS Code global state (no tokens stored)' instead of the misleading 'securely stored' phrasing. - Rename 'Sign Out from GitHub' command and button to 'Disconnect GitHub' to accurately reflect that only this extension's link to the session is broken, not the VS Code account itself. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/vscode-extension/README.md | 2 +- vscode-extension/package.json | 2 +- vscode-extension/src/extension.ts | 25 ++++++++++++++++++- .../src/webview/diagnostics/main.ts | 4 +-- vscode-extension/src/webview/usage/main.ts | 4 +-- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/vscode-extension/README.md b/docs/vscode-extension/README.md index 0c6df00f..44aa5a4e 100644 --- a/docs/vscode-extension/README.md +++ b/docs/vscode-extension/README.md @@ -31,7 +31,7 @@ Search for **"AI Engineering Fluency"** in the VS Code Extensions panel, or inst - **Opt-in Authentication**: Sign in with your configured GitHub account in VS Code - **Built-in VS Code Integration**: Uses VS Code's native authentication provider for GitHub -- **Secure Storage**: Authentication state is securely stored in VS Code's global state +- **Stored State**: Authentication state is stored in VS Code global state (no tokens stored) - **Future Features**: Foundation for upcoming GitHub-specific features such as: - Repository-specific usage tracking - Team collaboration features diff --git a/vscode-extension/package.json b/vscode-extension/package.json index db5cbf93..3a1f4a7c 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -132,7 +132,7 @@ }, { "command": "copilot-token-tracker.signOutGitHub", - "title": "Sign Out from GitHub", + "title": "Disconnect GitHub", "category": "AI Engineering Fluency" } ], diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 86d320f7..4bbfb684 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -883,6 +883,26 @@ class CopilotTokenTracker implements vscode.Disposable { // Restore GitHub authentication session if previously authenticated this._sessionRestorePromise = this.restoreGitHubSession(); + // Keep in-memory session in sync if the underlying VS Code auth session changes + // (e.g. user signs out of GitHub from the Accounts menu or token expires) + context.subscriptions.push( + vscode.authentication.onDidChangeSessions(async (e) => { + if (e.provider.id !== 'github') { return; } + if (this._githubSignedOutByUser) { return; } // user explicitly disconnected; don't auto-reconnect + const session = await vscode.authentication.getSession('github', ['read:user'], { createIfNone: false }); + if (session) { + this.githubSession = session; + await this.context.globalState.update('github.authenticated', true); + await this.context.globalState.update('github.username', session.account.label); + } else { + this.githubSession = undefined; + await this.context.globalState.update('github.authenticated', false); + await this.context.globalState.update('github.username', undefined); + this.log('GitHub session removed externally — clearing auth state'); + } + }) + ); + // Check GitHub Copilot extension status this.sessionDiscovery.checkCopilotExtension(); @@ -1214,6 +1234,9 @@ class CopilotTokenTracker implements vscode.Disposable { }, ); req.on('error', (e) => resolve({ prs: [], error: e.message })); + req.setTimeout(15000, () => { + req.destroy(new Error('Request timed out after 15 s')); + }); req.end(); }); } @@ -1233,7 +1256,7 @@ class CopilotTokenTracker implements vscode.Disposable { const msg = statusCode === 404 ? 'Repo not found or not accessible with current token' : statusCode === 403 - ? 'Access denied (private repo requires additional permissions)' + ? (error || 'Access denied (private repo requires additional permissions)') : error; return { prs: allPrs, error: msg }; } diff --git a/vscode-extension/src/webview/diagnostics/main.ts b/vscode-extension/src/webview/diagnostics/main.ts index e0e7838d..10ecfb9a 100644 --- a/vscode-extension/src/webview/diagnostics/main.ts +++ b/vscode-extension/src/webview/diagnostics/main.ts @@ -768,8 +768,8 @@ ${authenticated ? `
    ${authenticated ? ` ` : `