diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc25ea..ddbf37a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to the "copilot-token-tracker" extension will be documented Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [Unreleased] + +### 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.8] ### Changed diff --git a/README.md b/README.md index cbbaf66..d10aa71 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,19 @@ You can also use a **shared Azure Storage account** (a “shared storage server - **Smart Estimation**: Uses character-based analysis with model-specific ratios for token estimation - **Intelligent Caching**: Caches processed session files to speed up subsequent updates when files haven't changed - **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 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 "Copilot Token Tracker: Authenticate with GitHub", or access it through the Diagnostic Report's GitHub Auth tab. ### Cloud Backend (Opt-in) diff --git a/package-lock.json b/package-lock.json index 149eaf5..26a156b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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 9a8a26c..7608687 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,16 @@ "command": "copilot-token-tracker.clearCache", "title": "Clear Cache", "category": "Copilot Token Tracker" + }, + { + "command": "copilot-token-tracker.authenticateGitHub", + "title": "Authenticate with GitHub", + "category": "Copilot Token Tracker" + }, + { + "command": "copilot-token-tracker.signOutGitHub", + "title": "Sign Out from GitHub", + "category": "Copilot Token Tracker" } ], "configuration": { diff --git a/src/extension.ts b/src/extension.ts index da7c837..73e268a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -249,6 +249,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; + // Helper method to get repository URL from package.json private getRepositoryUrl(): string { const repoUrl = packageJson.repository?.url?.replace(/^git\+/, '').replace(/\.git$/, ''); @@ -470,6 +473,9 @@ class CopilotTokenTracker implements vscode.Disposable { // Load persisted cache from storage this.loadCacheFromStorage(); + // Restore GitHub authentication session if previously authenticated + this.restoreGitHubSession(); + // Check GitHub Copilot extension status this.checkCopilotExtension(); @@ -528,6 +534,38 @@ class CopilotTokenTracker implements vscode.Disposable { } } + /** + * 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); + } + } + private recheckCopilotExtensionsAfterDelay(): void { const copilotExtension = vscode.extensions.getExtension('GitHub.copilot'); const copilotChatExtension = vscode.extensions.getExtension('GitHub.copilot-chat'); @@ -542,6 +580,85 @@ class CopilotTokenTracker implements vscode.Disposable { } } + /** + * Authenticate with GitHub using VS Code's authentication API + * This creates a session that can be used for future GitHub-related features + */ + public async authenticateWithGitHub(): Promise { + try { + this.log('Attempting GitHub authentication...'); + + // Request authentication with GitHub + // Using 'read:user' scope as a minimal requirement + 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}`); + + // Store authentication state + 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 + * Checks both in-memory session and persisted state for accuracy + */ + public isGitHubAuthenticated(): boolean { + // 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); + } + + /** + * 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...'); @@ -4214,6 +4331,22 @@ class CopilotTokenTracker implements vscode.Disposable { case 'openSettings': await vscode.commands.executeCommand('workbench.action.openSettings', 'copilotTokenTracker.backend'); break; + case 'authenticateGitHub': + this.log('authenticateGitHub message received from diagnostics webview'); + await this.authenticateWithGitHub(); + // Refresh the diagnostics panel to show the updated auth status + if (this.diagnosticsPanel) { + await this.loadDiagnosticDataInBackground(this.diagnosticsPanel); + } + break; + case 'signOutGitHub': + this.log('signOutGitHub message received from diagnostics webview'); + await this.signOutFromGitHub(); + // Refresh the diagnostics panel to show the updated auth status + if (this.diagnosticsPanel) { + await this.loadDiagnosticDataInBackground(this.diagnosticsPanel); + } + break; } }); @@ -4290,6 +4423,9 @@ class CopilotTokenTracker implements vscode.Disposable { // Get backend storage info const backendStorageInfo = await this.getBackendStorageInfo(); + // 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'); @@ -4302,7 +4438,8 @@ class CopilotTokenTracker implements vscode.Disposable { report, sessionFiles: sessionFileData, sessionFolders, - backendStorageInfo + backendStorageInfo, + githubAuth: githubAuthStatus }); this.log('✅ Diagnostic data loaded and sent to webview'); @@ -4821,8 +4958,20 @@ export function activate(context: vscode.ExtensionContext) { await tokenTracker.clearCache(); }); + // 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, showDetailsCommand, showChartCommand, showUsageAnalysisCommand, generateDiagnosticReportCommand, clearCacheCommand, tokenTracker); + context.subscriptions.push(refreshCommand, showDetailsCommand, showChartCommand, showUsageAnalysisCommand, generateDiagnosticReportCommand, clearCacheCommand, authenticateGitHubCommand, signOutGitHubCommand, tokenTracker); tokenTracker.log('Extension activation complete'); } diff --git a/src/webview/diagnostics/main.ts b/src/webview/diagnostics/main.ts index 5233f5a..885be44 100644 --- a/src/webview/diagnostics/main.ts +++ b/src/webview/diagnostics/main.ts @@ -62,12 +62,18 @@ type BackendStorageInfo = { recordCount: number | null; }; +type GitHubAuthStatus = { + authenticated: boolean; + username?: string; +}; + type DiagnosticsData = { report: string; sessionFiles: { file: string; size: number; modified: string }[]; detailedSessionFiles?: SessionFileDetails[]; cacheInfo?: CacheInfo; backendStorageInfo?: BackendStorageInfo; + githubAuth?: GitHubAuthStatus; }; type DiagnosticsViewState = { @@ -338,6 +344,70 @@ function renderSessionTable(detailedFiles: SessionFileDetails[], isLoading: bool `; } +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 { if (!backendInfo) { return ` @@ -829,6 +899,7 @@ function renderLayout(data: DiagnosticsData): void { +
@@ -918,6 +989,10 @@ function renderLayout(data: DiagnosticsData): void {
${renderBackendStoragePanel(data.backendStorageInfo)}
+ +
+ ${renderGitHubAuthPanel(data.githubAuth)} +
`; @@ -1359,6 +1434,16 @@ function setupStorageLinkHandlers(): void { vscode.postMessage({ command: 'openSettings' }); }); + 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' }); + }); + setupSortHandlers(); setupEditorFilterHandlers(); setupFileLinks();