Skip to content

Commit bb9e510

Browse files
rajbosCopilot
andcommitted
feat: add optional GitHub authentication support
- GitHub account linking via VS Code's built-in auth provider - Auth is fully optional: never auto-prompts, all features work without signing in - New GitHub Auth tab in Diagnostic panel - New commands: authenticateGitHub and signOutGitHub - Session restore on startup uses createIfNone: false (no prompting) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7a051d6 commit bb9e510

File tree

5 files changed

+234
-0
lines changed

5 files changed

+234
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Track your GitHub Copilot token usage and AI Fluency across VS Code, Visual Stud
2424

2525
Real-time token usage in the status bar, fluency score dashboard, usage analysis, cloud sync, and more.
2626

27+
- **GitHub Authentication (Optional)**: Sign in with your GitHub account to unlock future features. All current functionality works without signing in.
28+
2729
```bash
2830
# Install from the VS Code Marketplace
2931
ext install RobBos.copilot-token-tracker

vscode-extension/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
66

77
## [Unreleased]
88

9+
### Added
10+
- GitHub authentication support using VS Code's built-in authentication provider (opt-in, signing in is optional)
11+
- New commands: "Authenticate with GitHub" and "Sign Out from GitHub"
12+
- GitHub Auth tab in Diagnostic Report panel showing authentication status
13+
914
## [0.0.27] - 2026-04-07
1015

1116
### ✨ Features & Improvements

vscode-extension/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@
124124
"command": "copilot-token-tracker.showEnvironmental",
125125
"title": "Show Environmental Impact",
126126
"category": "AI Engineering Fluency"
127+
},
128+
{
129+
"command": "copilot-token-tracker.authenticateGitHub",
130+
"title": "Authenticate with GitHub",
131+
"category": "AI Engineering Fluency"
132+
},
133+
{
134+
"command": "copilot-token-tracker.signOutGitHub",
135+
"title": "Sign Out from GitHub",
136+
"category": "AI Engineering Fluency"
127137
}
128138
],
129139
"configuration": {

vscode-extension/src/extension.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ class CopilotTokenTracker implements vscode.Disposable {
246246
// Backend facade instance for accessing table storage data
247247
private backend: BackendFacade | undefined;
248248

249+
// GitHub authentication session (undefined = not signed in)
250+
private githubSession: vscode.AuthenticationSession | undefined;
251+
249252
// Helper method to get repository URL from package.json
250253
private getRepositoryUrl(): string {
251254
return _getRepositoryUrl();
@@ -826,6 +829,9 @@ class CopilotTokenTracker implements vscode.Disposable {
826829
// Load persisted cache from storage
827830
this.cacheManager.loadCacheFromStorage();
828831

832+
// Silently restore GitHub session if user was previously authenticated
833+
this.restoreGitHubSession();
834+
829835
// Check GitHub Copilot extension status
830836
this.sessionDiscovery.checkCopilotExtension();
831837

@@ -898,6 +904,92 @@ class CopilotTokenTracker implements vscode.Disposable {
898904
}
899905
}
900906

907+
/**
908+
* Silently restore a GitHub authentication session on startup.
909+
* Only runs if the user was previously authenticated.
910+
* Uses createIfNone: false to never prompt the user automatically.
911+
*/
912+
private async restoreGitHubSession(): Promise<void> {
913+
try {
914+
const wasAuthenticated = this.context.globalState.get<boolean>('github.authenticated', false);
915+
if (wasAuthenticated) {
916+
this.log('Attempting to restore GitHub authentication session...');
917+
const session = await vscode.authentication.getSession('github', ['read:user'], { createIfNone: false });
918+
if (session) {
919+
this.githubSession = session;
920+
this.log(`✅ GitHub session restored for ${session.account.label}`);
921+
await this.context.globalState.update('github.username', session.account.label);
922+
} else {
923+
this.log('GitHub session not found — clearing authenticated state');
924+
await this.context.globalState.update('github.authenticated', false);
925+
await this.context.globalState.update('github.username', undefined);
926+
}
927+
}
928+
} catch (error) {
929+
this.warn('Failed to restore GitHub session: ' + String(error));
930+
await this.context.globalState.update('github.authenticated', false);
931+
await this.context.globalState.update('github.username', undefined);
932+
}
933+
}
934+
935+
/**
936+
* Authenticate with GitHub using VS Code's built-in authentication API.
937+
* Only called when the user explicitly requests it (e.g. clicks "Authenticate").
938+
*/
939+
public async authenticateWithGitHub(): Promise<void> {
940+
try {
941+
this.log('Attempting GitHub authentication...');
942+
const session = await vscode.authentication.getSession(
943+
'github',
944+
['read:user'],
945+
{ createIfNone: true },
946+
);
947+
if (session) {
948+
this.githubSession = session;
949+
this.log(`✅ Successfully authenticated as ${session.account.label}`);
950+
vscode.window.showInformationMessage(`GitHub authentication successful! Logged in as ${session.account.label}`);
951+
await this.context.globalState.update('github.authenticated', true);
952+
await this.context.globalState.update('github.username', session.account.label);
953+
}
954+
} catch (error) {
955+
this.error('GitHub authentication failed:', error);
956+
vscode.window.showErrorMessage('Failed to authenticate with GitHub. Please try again.');
957+
}
958+
}
959+
960+
/** Sign out from GitHub and clear the persisted authentication state. */
961+
public async signOutFromGitHub(): Promise<void> {
962+
try {
963+
this.log('Signing out from GitHub...');
964+
this.githubSession = undefined;
965+
await this.context.globalState.update('github.authenticated', false);
966+
await this.context.globalState.update('github.username', undefined);
967+
this.log('✅ Successfully signed out from GitHub');
968+
vscode.window.showInformationMessage('Signed out from GitHub successfully.');
969+
} catch (error) {
970+
this.error('Failed to sign out from GitHub:', error);
971+
vscode.window.showErrorMessage('Failed to sign out from GitHub.');
972+
}
973+
}
974+
975+
/** Return the current GitHub authentication status based on the in-memory session. */
976+
public getGitHubAuthStatus(): { authenticated: boolean; username?: string } {
977+
if (this.githubSession) {
978+
return { authenticated: true, username: this.githubSession.account.label };
979+
}
980+
return { authenticated: false };
981+
}
982+
983+
/** Return true if the user is currently authenticated with GitHub. */
984+
public isGitHubAuthenticated(): boolean {
985+
return this.githubSession !== undefined;
986+
}
987+
988+
/** Return the live GitHub authentication session, or undefined if not signed in. */
989+
public getGitHubSession(): vscode.AuthenticationSession | undefined {
990+
return this.githubSession;
991+
}
992+
901993
private async showFluencyScoreNewsBanner(): Promise<void> {
902994
const dismissedKey = 'news.fluencyScoreBanner.v1.dismissed';
903995
if (this.context.globalState.get<boolean>(dismissedKey)) {
@@ -6785,6 +6877,24 @@ ${hashtag}`;
67856877
});
67866878
}
67876879
break;
6880+
case "authenticateGitHub":
6881+
await this.dispatch('authenticateGitHub:diagnostics', async () => {
6882+
this.log('authenticateGitHub message received from diagnostics webview');
6883+
await this.authenticateWithGitHub();
6884+
if (this.diagnosticsPanel) {
6885+
await this.loadDiagnosticDataInBackground(this.diagnosticsPanel);
6886+
}
6887+
});
6888+
break;
6889+
case "signOutGitHub":
6890+
await this.dispatch('signOutGitHub:diagnostics', async () => {
6891+
this.log('signOutGitHub message received from diagnostics webview');
6892+
await this.signOutFromGitHub();
6893+
if (this.diagnosticsPanel) {
6894+
await this.loadDiagnosticDataInBackground(this.diagnosticsPanel);
6895+
}
6896+
});
6897+
break;
67886898
}
67896899
});
67906900

@@ -6926,6 +7036,7 @@ ${hashtag}`;
69267036
sessionFolders,
69277037
candidatePaths,
69287038
backendStorageInfo,
7039+
githubAuth: this.getGitHubAuthStatus(),
69297040
});
69307041

69317042
this.log("✅ Diagnostic data loaded and sent to webview");
@@ -7677,6 +7788,23 @@ export function activate(context: vscode.ExtensionContext) {
76777788
},
76787789
);
76797790

7791+
// Register the GitHub authentication commands
7792+
const authenticateGitHubCommand = vscode.commands.registerCommand(
7793+
"copilot-token-tracker.authenticateGitHub",
7794+
async () => {
7795+
tokenTracker.log("GitHub authentication command called");
7796+
await tokenTracker.authenticateWithGitHub();
7797+
},
7798+
);
7799+
7800+
const signOutGitHubCommand = vscode.commands.registerCommand(
7801+
"copilot-token-tracker.signOutGitHub",
7802+
async () => {
7803+
tokenTracker.log("GitHub sign out command called");
7804+
await tokenTracker.signOutFromGitHub();
7805+
},
7806+
);
7807+
76807808
// Add to subscriptions for proper cleanup
76817809
context.subscriptions.push(
76827810
refreshCommand,
@@ -7690,6 +7818,8 @@ export function activate(context: vscode.ExtensionContext) {
76907818
showEnvironmentalCommand,
76917819
generateDiagnosticReportCommand,
76927820
clearCacheCommand,
7821+
authenticateGitHubCommand,
7822+
signOutGitHubCommand,
76937823
tokenTracker,
76947824
);
76957825

vscode-extension/src/webview/diagnostics/main.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ type BackendStorageInfo = {
5858
recordCount: number | null;
5959
};
6060

61+
type GitHubAuthStatus = {
62+
authenticated: boolean;
63+
username?: string;
64+
};
65+
6166
type GlobalStateCounters = {
6267
openCount: number;
6368
unknownMcpOpenCount: number;
@@ -74,6 +79,7 @@ type DiagnosticsData = {
7479
backendConfigured?: boolean;
7580
isDebugMode?: boolean;
7681
globalStateCounters?: GlobalStateCounters;
82+
githubAuth?: GitHubAuthStatus;
7783
};
7884

7985
type DiagnosticsViewState = {
@@ -694,6 +700,59 @@ function renderDebugTab(counters: GlobalStateCounters | undefined): string {
694700
</div>`;
695701
}
696702

703+
function renderGitHubAuthPanel(githubAuth: GitHubAuthStatus | undefined): string {
704+
const authenticated = githubAuth?.authenticated ?? false;
705+
const username = githubAuth?.username ?? '';
706+
const statusColor = authenticated ? '#2d6a4f' : '#666';
707+
const statusIcon = authenticated ? '✅' : '⚪';
708+
const statusText = authenticated ? 'Authenticated' : 'Not signed in';
709+
710+
return `
711+
<div class="info-box">
712+
<div class="info-box-title">🔑 GitHub Authentication</div>
713+
<div>
714+
Authenticate with GitHub to unlock additional features in future releases.
715+
Signing in is completely optional — all current features work without an account.
716+
</div>
717+
</div>
718+
719+
<div class="summary-cards">
720+
<div class="summary-card" style="border-left: 4px solid ${statusColor};">
721+
<div class="summary-label">${statusIcon} Status</div>
722+
<div class="summary-value" style="font-size: 16px; color: ${statusColor};">${statusText}</div>
723+
</div>
724+
${authenticated ? `
725+
<div class="summary-card">
726+
<div class="summary-label">👤 Signed in as</div>
727+
<div class="summary-value" style="font-size: 16px;">${escapeHtml(username)}</div>
728+
</div>
729+
` : ''}
730+
</div>
731+
732+
<div style="margin-top: 24px;">
733+
<p style="color: #999; font-size: 12px; margin-bottom: 16px;">
734+
${authenticated
735+
? 'You are signed in with GitHub. Future features such as repository-specific tracking and team collaboration will use this account.'
736+
: "Sign in with your GitHub account to unlock future features. This uses VS Code's built-in authentication and will not affect any existing functionality."}
737+
</p>
738+
</div>
739+
740+
<div class="button-group">
741+
${authenticated ? `
742+
<button class="button secondary" id="btn-sign-out-github">
743+
<span>🚪</span>
744+
<span>Sign Out</span>
745+
</button>
746+
` : `
747+
<button class="button" id="btn-authenticate-github">
748+
<span>🔑</span>
749+
<span>Authenticate with GitHub</span>
750+
</button>
751+
`}
752+
</div>
753+
`;
754+
}
755+
697756
function renderBackendStoragePanel(
698757
backendInfo: BackendStorageInfo | undefined,
699758
): string {
@@ -974,6 +1033,7 @@ function renderLayout(data: DiagnosticsData): void {
9741033
<button class="tab" data-tab="sessions">📁 Session Files (${detailedFiles.length})</button>
9751034
<button class="tab" data-tab="cache">💾 Cache</button>
9761035
<button class="tab" data-tab="backend">☁️ Azure Storage</button>
1036+
<button class="tab" data-tab="github">🔑 GitHub Auth</button>
9771037
<button class="tab" data-tab="display">⚙️ Settings</button>
9781038
${data.isDebugMode ? '<button class="tab" data-tab="debug">🐛 Debug</button>' : ''}
9791039
</div>
@@ -1065,6 +1125,9 @@ function renderLayout(data: DiagnosticsData): void {
10651125
<div id="tab-backend" class="tab-content">
10661126
${renderBackendStoragePanel(data.backendStorageInfo)}
10671127
</div>
1128+
<div id="tab-github" class="tab-content">
1129+
${renderGitHubAuthPanel(data.githubAuth)}
1130+
</div>
10681131
<div id="tab-display" class="tab-content">
10691132
<div class="info-box">
10701133
<div class="info-box-title">⚙️ Display Settings</div>
@@ -1125,6 +1188,15 @@ function renderLayout(data: DiagnosticsData): void {
11251188
console.warn("diagnosticDataLoaded received but backendStorageInfo is missing or undefined");
11261189
}
11271190

1191+
// Update GitHub auth panel if status is provided
1192+
if (message.githubAuth !== undefined) {
1193+
const githubTabContent = document.getElementById("tab-github");
1194+
if (githubTabContent) {
1195+
githubTabContent.innerHTML = renderGitHubAuthPanel(message.githubAuth);
1196+
setupGitHubAuthHandlers();
1197+
}
1198+
}
1199+
11281200
// Update session folders if provided
11291201
if (message.sessionFolders && message.sessionFolders.length > 0) {
11301202
const reportTabContent = document.getElementById("tab-report");
@@ -1383,6 +1455,20 @@ function renderLayout(data: DiagnosticsData): void {
13831455
}
13841456
});
13851457

1458+
// Wire up GitHub authentication button handlers
1459+
function setupGitHubAuthHandlers(): void {
1460+
document
1461+
.getElementById("btn-authenticate-github")
1462+
?.addEventListener("click", () => {
1463+
vscode.postMessage({ command: "authenticateGitHub" });
1464+
});
1465+
document
1466+
.getElementById("btn-sign-out-github")
1467+
?.addEventListener("click", () => {
1468+
vscode.postMessage({ command: "signOutGitHub" });
1469+
});
1470+
}
1471+
13861472
// Handle open storage link clicks
13871473
function setupStorageLinkHandlers(): void {
13881474
document.querySelectorAll(".open-storage-link").forEach((link) => {
@@ -1720,6 +1806,7 @@ function renderLayout(data: DiagnosticsData): void {
17201806
setupBackendButtonHandlers();
17211807
setupFileLinks();
17221808
setupStorageLinkHandlers();
1809+
setupGitHubAuthHandlers();
17231810

17241811
// Restore active tab from saved state, with fallback to default
17251812
const savedState = vscode.getState();

0 commit comments

Comments
 (0)