Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ Copilot Insights uses VS Code's built-in GitHub authentication provider and requ

- `https://api.github.com/copilot_internal/user`

For GitHub Enterprise, configure:

- `copilotInsights.authProvider`: set to `github-enterprise` (or keep `auto`)
- `copilotInsights.apiBaseUrl`: your enterprise API base URL (for example `https://ghe.example.com/api/v3`)

The extension appends `/copilot_internal/user` to the configured API base URL.

The extension stores a small local history of recent AI credit snapshots in VS Code global state so it can show trend and prediction views. No external service is used by this extension to store your quota history.

## Troubleshooting
Expand All @@ -169,6 +176,7 @@ The extension stores a small local history of recent AI credit snapshots in VS C
- Make sure you are signed into the correct GitHub account in VS Code.
- Confirm your account has GitHub Copilot access.
- Trigger a manual refresh from the view title bar or command palette.
- For GitHub Enterprise, set `copilotInsights.authProvider` to `github-enterprise` and configure `copilotInsights.apiBaseUrl`.

### GitHub API returns 403 or 404

Expand Down
8 changes: 7 additions & 1 deletion l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,11 @@
"custom limit": "custom limit",
"monthly quota": "monthly quota",
"Copilot AI Credits are at {0}% of your {1} (alert threshold: {2}%).": "Copilot AI Credits are at {0}% of your {1} (alert threshold: {2}%).",
"Open details": "Open details"
"Open details": "Open details",
"Sign in with GHE": "Sign in with GHE",
"GitHub.com": "GitHub.com",
"Use your github.com account": "Use your github.com account",
"GitHub Enterprise": "GitHub Enterprise",
"Use your GitHub Enterprise account": "Use your GitHub Enterprise account",
"Choose an authentication provider for Copilot Insights": "Choose an authentication provider for Copilot Insights"
}
13 changes: 13 additions & 0 deletions media/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,22 @@ body {
font-size: 13px;
font-family: var(--vscode-font-family);
}
.signin-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
.signin button.secondary {
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
}
.signin button:hover {
background-color: var(--vscode-button-hoverBackground);
}
.signin button.secondary:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.error {
color: var(--vscode-errorForeground);
padding: 20px;
Expand Down
7 changes: 5 additions & 2 deletions media/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,11 @@
}
});

document.getElementById("signInButton").addEventListener("click", () => {
vscode.postMessage({ command: "signIn" });
document.getElementById("signInGithubButton").addEventListener("click", () => {
vscode.postMessage({ command: "signIn", providerId: "github" });
});
document.getElementById("signInGheButton").addEventListener("click", () => {
vscode.postMessage({ command: "signIn", providerId: "github-enterprise" });
});
document.getElementById("copyButton").addEventListener("click", () => {
vscode.postMessage({ command: "copyToClipboard" });
Expand Down
20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,26 @@
"minimum": 0,
"description": "%copilotInsights.config.pollingIntervalSeconds.description%"
},
"copilotInsights.authProvider": {
"type": "string",
"default": "auto",
"enum": [
"auto",
"github",
"github-enterprise"
],
"enumDescriptions": [
"%copilotInsights.config.authProvider.auto%",
"%copilotInsights.config.authProvider.github%",
"%copilotInsights.config.authProvider.githubEnterprise%"
],
"description": "%copilotInsights.config.authProvider.description%"
},
"copilotInsights.apiBaseUrl": {
"type": "string",
"default": "https://api.github.com",
"description": "%copilotInsights.config.apiBaseUrl.description%"
},
"copilotInsights.statusBarLocation": {
"type": "string",
"default": "right",
Expand Down
5 changes: 5 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
"copilotInsights.config.progressBarMode.remaining": "Show remaining quota (bar empties as you use requests)",
"copilotInsights.config.progressBarMode.used": "Show used quota (bar fills as you use requests)",
"copilotInsights.config.pollingIntervalSeconds.description": "Refresh Copilot quota data automatically every N seconds. Set to 0 to disable background polling.",
"copilotInsights.config.authProvider.description": "Authentication provider used by Copilot Insights. Use 'auto' to reuse existing sessions from GitHub.com or GitHub Enterprise.",
"copilotInsights.config.authProvider.auto": "Auto-detect from existing sessions and ask when sign-in is needed",
"copilotInsights.config.authProvider.github": "Always use GitHub.com authentication",
"copilotInsights.config.authProvider.githubEnterprise": "Always use GitHub Enterprise authentication",
"copilotInsights.config.apiBaseUrl.description": "Base URL used for the Copilot internal API endpoint. Use https://api.github.com for GitHub.com or your enterprise API URL (for example https://ghe.example.com/api/v3).",
"copilotInsights.config.statusBarLocation.description": "Controls where the Copilot status bar appears: 'right' (original location), 'left' (bottom status bar), or 'both' (show on both sides).",
"copilotInsights.config.statusBarLocation.right": "Show status bar on the right side (original location)",
"copilotInsights.config.statusBarLocation.left": "Show status bar on the left side (bottom status bar)",
Expand Down
16 changes: 13 additions & 3 deletions src/api/copilotApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { CopilotUserData } from "../types";

const COPILOT_USER_ENDPOINT = "https://api.github.com/copilot_internal/user";
const DEFAULT_COPILOT_API_BASE_URL = "https://api.github.com";

function buildCopilotUserEndpoint(apiBaseUrl?: string): string {
const trimmedBase = (apiBaseUrl ?? DEFAULT_COPILOT_API_BASE_URL)
.trim()
.replace(/\/+$/, "");
return `${trimmedBase}/copilot_internal/user`;
}

function normalizeCopilotPlan(plan: unknown): string {
const value = typeof plan === "string" ? plan.trim() : "";
Expand All @@ -14,8 +21,11 @@ function normalizeCopilotPlan(plan: unknown): string {
* Fetches and normalizes the Copilot account/quota data for the
* authenticated user from GitHub's (internal, undocumented) endpoint.
*/
export async function fetchCopilotUserData(accessToken: string): Promise<CopilotUserData> {
const response = await fetch(COPILOT_USER_ENDPOINT, {
export async function fetchCopilotUserData(
accessToken: string,
apiBaseUrl?: string
): Promise<CopilotUserData> {
const response = await fetch(buildCopilotUserEndpoint(apiBaseUrl), {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
Expand Down
104 changes: 88 additions & 16 deletions src/ui/webview/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
WebviewStateMessage,
} from "./render";

const GITHUB_PROVIDER_IDS = ["github", "github-enterprise"] as const;
type GithubProviderId = (typeof GITHUB_PROVIDER_IDS)[number];

export class CopilotInsightsViewProvider implements vscode.WebviewViewProvider, vscode.Disposable {
public static readonly viewType = "copilotInsights.sidebarView";

Expand Down Expand Up @@ -70,13 +73,11 @@ export class CopilotInsightsViewProvider implements vscode.WebviewViewProvider,
this._context.subscriptions.push(configurationChangeDisposable);

// Refresh silently when GitHub authentication sessions change (sign-in/out)
const sessionChangeDisposable = vscode.authentication.onDidChangeSessions(
(event) => {
if (event.provider.id === "github") {
void this.loadCopilotData({ silent: true });
}
const sessionChangeDisposable = vscode.authentication.onDidChangeSessions((event) => {
if (GITHUB_PROVIDER_IDS.includes(event.provider.id as GithubProviderId)) {
void this.loadCopilotData({ silent: true });
}
);
});
this._context.subscriptions.push(sessionChangeDisposable);

this._restartPolling();
Expand Down Expand Up @@ -191,9 +192,14 @@ export class CopilotInsightsViewProvider implements vscode.WebviewViewProvider,
);
}
break;
case "signIn":
await this.loadCopilotData();
case "signIn": {
const preferredProvider =
message.providerId === "github" || message.providerId === "github-enterprise"
? message.providerId
: undefined;
await this.loadCopilotData({ preferredProvider });
break;
}
}
});

Expand All @@ -208,7 +214,7 @@ export class CopilotInsightsViewProvider implements vscode.WebviewViewProvider,
this.loadCopilotData();
}

public async loadCopilotData(options: { silent?: boolean } = {}) {
public async loadCopilotData(options: { silent?: boolean; preferredProvider?: GithubProviderId } = {}) {
if (this._isLoadingCopilotData) {
return;
}
Expand All @@ -219,12 +225,9 @@ export class CopilotInsightsViewProvider implements vscode.WebviewViewProvider,
// Get GitHub authentication session.
// Silent loads (startup, background polling) never prompt the user;
// interactive loads (opening the view, manual refresh) may show the sign-in flow.
const session = await vscode.authentication.getSession(
"github",
["user:email"],
options.silent
? { createIfNone: false, silent: true }
: { createIfNone: true }
const session = await this._getGitHubSession(
options.silent === true,
options.preferredProvider
);

if (!session) {
Expand All @@ -241,7 +244,10 @@ export class CopilotInsightsViewProvider implements vscode.WebviewViewProvider,
return;
}

const data = await fetchCopilotUserData(session.accessToken);
const apiBaseUrl = vscode.workspace
.getConfiguration("copilotInsights")
.get<string>("apiBaseUrl", "https://api.github.com");
const data = await fetchCopilotUserData(session.accessToken, apiBaseUrl);

// Record snapshot for history tracking (per GitHub account)
this._snapshots.setAccount(data.login);
Expand Down Expand Up @@ -291,6 +297,72 @@ export class CopilotInsightsViewProvider implements vscode.WebviewViewProvider,
this._postState(model);
}

private async _getGitHubSession(
silent: boolean,
preferredProvider?: GithubProviderId
): Promise<vscode.AuthenticationSession | undefined> {
const config = vscode.workspace.getConfiguration("copilotInsights");
const authProvider = config.get<string>("authProvider", "auto");
const scopes = ["user:email"];

const configuredProvider =
authProvider === "github" || authProvider === "github-enterprise"
? authProvider
: undefined;

const candidates = preferredProvider
? [preferredProvider]
: configuredProvider
? [configuredProvider]
: [...GITHUB_PROVIDER_IDS];

for (const providerId of candidates) {
const existingSession = await vscode.authentication.getSession(providerId, scopes, {
createIfNone: false,
silent: true,
});
if (existingSession) {
return existingSession;
}
}

if (silent) {
return undefined;
}

const providerForInteractiveSignIn =
preferredProvider ?? configuredProvider ?? (await this._pickAuthProviderForSignIn());
if (!providerForInteractiveSignIn) {
return undefined;
}

return vscode.authentication.getSession(providerForInteractiveSignIn, scopes, {
createIfNone: true,
});
}

private async _pickAuthProviderForSignIn(): Promise<GithubProviderId | undefined> {
const choice = await vscode.window.showQuickPick(
[
{
label: vscode.l10n.t("GitHub.com"),
description: vscode.l10n.t("Use your github.com account"),
providerId: "github" as const,
},
{
label: vscode.l10n.t("GitHub Enterprise"),
description: vscode.l10n.t("Use your GitHub Enterprise account"),
providerId: "github-enterprise" as const,
},
],
{
placeHolder: vscode.l10n.t("Choose an authentication provider for Copilot Insights"),
}
);

return choice?.providerId;
}

private _publishError(error: string) {
this._statusBar.showError(error);
this._postState({ state: "error", message: error });
Expand Down
5 changes: 4 additions & 1 deletion src/ui/webview/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ export function renderShellHtml(webview: vscode.Webview, extensionUri: vscode.Ur

<div id="state-signin" class="state signin hidden">
<p>${t("Sign in with GitHub to see your Copilot plan, quotas, and usage insights.")}</p>
<button id="signInButton">${t("Sign in with GitHub")}</button>
<div class="signin-actions">
<button id="signInGithubButton">${t("Sign in with GitHub")}</button>
<button id="signInGheButton" class="secondary">${t("Sign in with GHE")}</button>
</div>
</div>

<div id="state-error" class="state hidden">
Expand Down
Loading