Build a VS Code Extension for AI Agent PRs: 14 Steps [2026]

VS Code shipped version 1.139.1 on September 25, 2026, and the release note is telling: the editor now needs to handle “large agent session lists” faster because developers are running more autonomous coding agents at once than the UI was originally built for. Three days earlier, Cursor launched two bots, Rollouts and Security Review, that watch pull requests through deployment and flag exploitable bugs automatically. Put those two data points together and a gap becomes obvious. The editors have agent sessions, PR automation, and security scanning built in, but there is no unified way to see all of it from one place, especially if a team mixes Cursor and VS Code across its engineers.

This tutorial builds that missing piece: a VS Code extension that lists active AI agent sessions, tracks the pull requests those agents opened, and surfaces security-review findings in a single sidebar view. It works in both VS Code and Cursor, since Cursor is a fork of VS Code and shares its extension API. By the end you will have a working, installable extension, a GitHub Actions pipeline for packaging it, and a checklist for publishing it to the Visual Studio Marketplace and Open VSX.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why build an agent-session and PR-review extension in 2026

According to the 2025 Stack Overflow Developer Survey, 84% of respondents are using or planning to use AI tools in their development process, and 51% of professional developers report using AI tools daily. The same survey found that 31% of developers are already using AI agents specifically, not just autocomplete or chat, and 69% of those who used agents at work said they saw a real productivity increase. That is the upside. The friction shows up right after: 46% of developers said they do not trust the accuracy of AI tool output, and separate InfoWorld reporting on the same survey data found that 66% of developers cited “dealing with AI solutions that are almost right, but not quite” as a top frustration, with 45% saying debugging AI-generated code actually takes more time than writing it themselves.

That trust gap is exactly what a visibility extension addresses. If an agent opens a pull request while you are in a meeting, you want to see it land in a dashboard, not discover it three days later buried in your GitHub notifications. Cursor’s own changelog frames its new bots the same way: Rollouts “watches every change as it deploys and reports its health per environment,” and Security Review “reports exploitable bugs on every pull request,” covering injection flaws, authentication bypasses, committed secrets, SSRF, unsafe deserialization, and vulnerable dependency bumps. Those are server-side features running on Cursor’s Teams and Enterprise plans. The extension in this tutorial is the client-side complement: a way to pull that data (and equivalent GitHub PR data if you are not on Cursor’s paid tiers) into the editor sidebar where the agent work actually happens.

This gap matters more once you look at how the underlying editor has changed shape over the last two release cycles. Historically, a VS Code extension that wanted to show “what is my code doing right now” only had to worry about the local workspace: open files, the debugger, maybe the integrated terminal. Agent sessions break that model, because the work an agent does can span a cloud sandbox, a remote Dev Container, and a GitHub pull request simultaneously, often without the developer’s editor window being open at all. An extension that only reflects local workspace state is now, by definition, incomplete. That is the underlying reason this tutorial reaches outside the local file system into the GitHub REST API from Step 4 onward, rather than trying to infer agent activity purely from files on disk.

There is also a growth angle worth noting. McKinsey’s 2024 analysis on generative AI found that 65,000 public generative AI projects were created on GitHub in 2023, a jump of almost 250% over the prior year. Extension tooling that manages AI agent workflows sits directly in that growth curve, and building one is one of the more practical ways to learn the modern VS Code Extension API, since it touches the Agent Sessions API, the GitHub REST API, custom webviews, and the Language Model Tools API all in one project.

Prerequisites and exact versions

Install and confirm these versions before starting. Extension APIs move fast, and running an older editor build is the single most common reason this kind of tutorial fails halfway through.

ToolMinimum version requiredCheck command
Visual Studio Code (or Cursor)1.139.1 (VS Code) / latest Cursor buildcode --version
Node.js20.x LTS or newernode --version
npm10.x or newernpm --version
Yeoman + VS Code generatorgenerator-code 1.9.xyo --version
vsce (packaging CLI)3.xvsce --version
Git2.40+git --version
GitHub personal access tokenFine-grained, repo + pull request scopen/a
TypeScript5.6+tsc --version

You will also want a GitHub repository with at least one open pull request to test against, and either a Cursor Teams/Enterprise account (for live Rollouts and Security Review data) or just a standard GitHub repo (the extension degrades gracefully to plain PR status checks if Cursor’s bot APIs are not available to your account). Nothing here requires a paid Cursor plan to follow along; the Cursor-specific integration is presented as an optional module in Step 9.

Step 1: Scaffold the extension project

Microsoft’s official Yeoman generator documentation is still the fastest way to get a compliant extension skeleton, and it works identically inside Cursor since Cursor consumes the same vscode npm package and manifest schema. Install the generator globally, then scaffold a new TypeScript extension.

npm install -g yo generator-code
yo code

# When prompted, choose:
# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? agent-pr-control-center
# ? What's the identifier of your extension? agent-pr-control-center
# ? Bundle the source code with webpack? Yes
# ? Initialize a git repository? Yes
# ? Which package manager to use? npm

Once scaffolding finishes, open the folder in your editor and confirm it launches in the Extension Development Host with F5. You should see an empty “Hello World” command register in the Command Palette. That confirms your toolchain (Node, npm, esbuild/webpack, and the extension host) is wired correctly before you add any real logic.

Step 2: Define the extension manifest and activation events

Open package.json and replace the generated contributes block with the sidebar view, commands, and configuration settings this extension needs. The viewsContainers entry creates a new icon in the Activity Bar; the views entry attaches your tree view to it.

{
 "name": "agent-pr-control-center",
 "displayName": "Agent PR Control Center",
 "description": "Track AI agent sessions, pull requests, and security review findings from one sidebar",
 "version": "0.1.0",
 "engines": { "vscode": "^1.139.0" },
 "activationEvents": ["onStartupFinished"],
 "main": "./dist/extension.js",
 "contributes": {
 "viewsContainers": {
 "activitybar": [
 {
 "id": "agentPrControlCenter",
 "title": "Agent PR Control",
 "icon": "media/robot.svg"
 }
 ]
 },
 "views": {
 "agentPrControlCenter": [
 { "id": "agentSessions", "name": "Active Agent Sessions" },
 { "id": "pullRequests", "name": "Open Pull Requests" },
 { "id": "securityFindings", "name": "Security Review Findings" }
 ]
 },
 "commands": [
 { "command": "agentPrControlCenter.refresh", "title": "Refresh Agent PR Data" },
 { "command": "agentPrControlCenter.openPr", "title": "Open Pull Request in Browser" }
 ],
 "configuration": {
 "title": "Agent PR Control Center",
 "properties": {
 "agentPrControlCenter.githubToken": {
 "type": "string",
 "default": "",
 "description": "Fine-grained GitHub PAT with pull request read access"
 },
 "agentPrControlCenter.repository": {
 "type": "string",
 "default": "",
 "description": "owner/repo to monitor"
 },
 "agentPrControlCenter.pollIntervalSeconds": {
 "type": "number",
 "default": 60
 }
 }
 }
 }
}

Note the onStartupFinished activation event rather than *. VS Code’s own extension guidelines flag wildcard activation as a performance anti-pattern because it forces your code to load on every window open even when nobody uses the feature that session. With three tree views and a polling timer, lazy activation keeps startup time predictable.

Step 3: Build the Agent Sessions tree data provider

VS Code 1.138 introduced agent sessions inside Dev Containers, and 1.139 extended that support to remote projects while making large session lists render faster. Your extension will not have direct API access to Copilot’s internal agent session store (that is intentionally not exposed to third parties), so instead you track sessions your own automation launches, tagging each with metadata your extension controls. This is the practical approach every extension in this space takes: model the domain object yourself, then reconcile it against whatever external signal you can read (branch names, PR authorship, commit trailers).

import * as vscode from 'vscode';

interface AgentSession {
 id: string;
 label: string;
 branch: string;
 status: 'running' | 'awaiting-review' | 'merged' | 'failed';
 startedAt: number;
}

export class AgentSessionProvider implements vscode.TreeDataProvider<AgentSession> {
 private _onDidChangeTreeData = new vscode.EventEmitter<void>();
 readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
 private sessions: AgentSession[] = [];

 refresh(sessions: AgentSession[]): void {
 this.sessions = sessions;
 this._onDidChangeTreeData.fire();
 }

 getTreeItem(element: AgentSession): vscode.TreeItem {
 const item = new vscode.TreeItem(
 `${element.label} (${element.branch})`,
 vscode.TreeItemCollapsibleState.None
 );
 item.description = element.status;
 item.iconPath = new vscode.ThemeIcon(
 element.status === 'failed' ? 'error' :
 element.status === 'merged' ? 'check' : 'sync~spin'
 );
 return item;
 }

 getChildren(): AgentSession[] {
 return this.sessions;
 }
}

Register the provider in your activation function and it immediately renders in the sidebar, even with an empty array, which is useful for confirming the view container wiring before you connect real data.

Step 4: Connect the GitHub REST API for pull request data

GitHub’s REST API for pull requests is the backbone for the second tree view. Rather than pull in the full Octokit SDK for a handful of endpoints, a lightweight fetch wrapper keeps your bundle small, which matters because VS Code extensions load synchronously on activation and users notice slow-starting extensions immediately.

async function fetchOpenPullRequests(owner: string, repo: string, token: string) {
 const response = await fetch(
 `https://api.github.com/repos/${owner}/${repo}/pulls?state=open&per_page=30`,
 {
 headers: {
 Authorization: `Bearer ${token}`,
 Accept: 'application/vnd.github+json',
 'X-GitHub-Api-Version': '2022-11-28'
 }
 }
 );

 if (!response.ok) {
 throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
 }

 const pulls = await response.json();
 return pulls.map((pr: any) => ({
 id: pr.number,
 title: pr.title,
 author: pr.user.login,
 branch: pr.head.ref,
 url: pr.html_url,
 draft: pr.draft
 }));
}

Store the token using VS Code’s SecretStorage API rather than plain settings, since settings.json can sync to cloud profiles or get committed accidentally in workspace-level config. Never read the token from the agentPrControlCenter.githubToken setting directly in production code; that setting only exists in Step 2 as a placeholder pattern many tutorials use incorrectly. The corrected, secure pattern is in Step 8.

Step 5: Wire up polling and manual refresh

A background poll keeps the sidebar current without the user manually triggering a refresh every time an agent pushes a commit. Use setInterval guarded by a disposable so the timer is cleaned up correctly when the extension deactivates, and always expose the same logic through a command so users can force an immediate refresh.

export function activate(context: vscode.ExtensionContext) {
 const sessionProvider = new AgentSessionProvider();
 const prProvider = new PullRequestProvider();

 vscode.window.registerTreeDataProvider('agentSessions', sessionProvider);
 vscode.window.registerTreeDataProvider('pullRequests', prProvider);

 const config = vscode.workspace.getConfiguration('agentPrControlCenter');
 const intervalMs = (config.get<number>('pollIntervalSeconds') || 60) * 1000;

 const poll = async () => {
 try {
 await refreshAllData(sessionProvider, prProvider, context);
 } catch (err) {
 console.error('Agent PR Control Center poll failed:', err);
 }
 };

 const timer = setInterval(poll, intervalMs);
 context.subscriptions.push({ dispose: () => clearInterval(timer) });

 context.subscriptions.push(
 vscode.commands.registerCommand('agentPrControlCenter.refresh', poll)
 );

 poll();
}

Sixty seconds is a sane default poll interval. GitHub’s REST API applies a 5,000 requests-per-hour rate limit for authenticated requests on personal accounts, and a 60-second interval consumes roughly 60 requests per hour per monitored repository, well within budget even if a team runs the extension across a handful of repos simultaneously.

Step 6: Add a webview panel for security review findings

Tree views work well for lists, but a security finding needs more structure than a single line: a description, a severity, a file path, and a remediation suggestion. A webview panel gives you full HTML/CSS control for that richer layout. Cursor’s Security Review bot reports findings that include injection flaws, authentication bypasses, committed secrets, SSRF, unsafe deserialization, and vulnerable dependency changes, and your webview should render each finding with a visible severity tag so a reviewer can triage in seconds.

function renderFindingsHtml(findings: SecurityFinding[]): string {
 const rows = findings.map(f => `
 <tr class="severity-${f.severity}">
 <td>${f.severity.toUpperCase()}</td>
 <td>${escapeHtml(f.title)}</td>
 <td>${escapeHtml(f.file)}:${f.line}</td>
 <td><a href="${f.prUrl}">View PR</a></td>
 </tr>`).join('');

 return `<!DOCTYPE html>
 <html><body>
 <table>
 <tr><th>Severity</th><th>Finding</th><th>Location</th><th>Link</th></tr>
 ${rows}
 </table>
 </body></html>`;
}

function escapeHtml(s: string): string {
 return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}

Always run untrusted text (PR titles, finding descriptions, file names) through an escaping function before interpolating it into webview HTML. Webviews execute arbitrary script if you skip this step, and a malicious commit message or PR title becomes a cross-site scripting vector inside your own extension. This is the most commonly skipped step in webview tutorials and the one most likely to fail a Marketplace security review.

Step 7: Register the security-conscious content security policy

Beyond escaping strings, VS Code requires webviews to declare a Content Security Policy or the panel silently fails to render inline scripts and styles under stricter editor settings. Set a nonce-based CSP so only your own script tag executes.

function getNonce(): string {
 let text = '';
 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
 for (let i = 0; i < 32; i++) {
 text += chars.charAt(Math.floor(Math.random() * chars.length));
 }
 return text;
}

const nonce = getNonce();
const csp = `default-src 'none'; style-src ${panel.webview.cspSource}; script-src 'nonce-${nonce}';`;
// Embed <meta http-equiv="Content-Security-Policy" content="${csp}"> in your HTML head
// and add nonce="${nonce}" to your <script> tag.

Step 8: Store secrets correctly with SecretStorage

Now correct the placeholder from Step 4. VS Code’s ExtensionContext.secrets API stores values in the OS-native credential store (Keychain on macOS, Credential Manager on Windows, libsecret on Linux) instead of plain-text settings files. Prompt for the token once, then read it from secure storage on every subsequent poll.

async function ensureGitHubToken(context: vscode.ExtensionContext): Promise<string> {
 let token = await context.secrets.get('githubPat');

 if (!token) {
 token = await vscode.window.showInputBox({
 prompt: 'Enter a fine-grained GitHub PAT with pull request read access',
 password: true,
 ignoreFocusOut: true
 });

 if (token) {
 await context.secrets.store('githubPat', token);
 }
 }

 if (!token) {
 throw new Error('GitHub token is required to fetch pull request data');
 }

 return token;
}

Add a companion command, agentPrControlCenter.clearToken, that calls context.secrets.delete('githubPat'). Every extension that stores credentials needs an equally visible way to remove them, both for security hygiene and because Marketplace reviewers specifically check for this when auditing extensions that request PAT scopes.

Step 9: Optional module – reading Cursor Rollouts and Security Review status

If your team is on a Cursor Teams or Enterprise plan, Cursor’s Rollouts bot already attaches a monitoring plan as a PR comment and tracks deploy events per environment, reporting each as verified healthy, regression detected, or inconclusive. Security Review posts its findings as PR review comments in a structured format. Because both bots communicate through standard GitHub PR comments and check runs rather than a separate private API, your extension can parse them using the same GitHub REST endpoints from Step 4, specifically the list-review-comments and list-check-runs endpoints, without needing direct Cursor API access.

async function fetchCursorBotComments(owner: string, repo: string, prNumber: number, token: string) {
 const res = await fetch(
 `https://api.github.com/repos/${owner}/${repo}/issues/${prNumber}/comments`,
 { headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' } }
 );
 const comments = await res.json();
 return comments.filter((c: any) =>
 c.user.login === 'cursor[bot]' || c.user.login.includes('cursor')
 );
}

Parse the comment body for the status keywords (“verified healthy,” “regression detected,” “inconclusive”) and surface a colored badge in your tree view. This pattern, treating a bot’s PR comment as a structured data source, is the same technique used to integrate with Dependabot, CodeQL, and most other GitHub-native automation, so it transfers directly to other integrations you might add later.

Step 10: Test inside the Extension Development Host

Press F5 to launch a new window with your extension loaded. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P) and run “Agent PR Control Center: Refresh.” Check the Debug Console in your original window for any thrown errors, since uncaught exceptions in activation silently disable the extension without always showing a visible error toast.

Expected output in the sidebar after a successful first poll against a real repository:

AGENT PR CONTROL
├── Active Agent Sessions
│ └── (empty until you wire in your own session source)
├── Open Pull Requests
│ ├── #482 Fix rate limiter race condition (feature/rate-limit-fix)
│ ├── #481 Add retry backoff to webhook dispatcher (fix/webhook-retry)
│ └── #479 [DRAFT] Migrate auth middleware to JWT (draft/jwt-auth)
└── Security Review Findings
 ├── HIGH Hardcoded API key in config/staging.yml:14
 └── MEDIUM Missing input validation on /api/upload endpoint

If the tree views render but stay empty despite a successful network call, check that your refresh() method actually fires _onDidChangeTreeData.event, since that emitter is what tells VS Code’s UI layer to re-query getChildren().

Step 11: Package the extension with vsce

Once functionality is stable, bundle and package into a distributable .vsix file. Add a .vscodeignore file first so source maps, test files, and node_modules dev dependencies do not bloat the package.

npm install -g @vscode/vsce
npm run compile
vsce package

# Output: agent-pr-control-center-0.1.0.vsix

Install the packaged file locally to verify it behaves identically outside the debug host: code --install-extension agent-pr-control-center-0.1.0.vsix. This step catches bugs related to bundling paths and asset references that the Extension Development Host sometimes masks because it runs from source rather than the compiled bundle.

Step 12: Automate packaging with GitHub Actions

A CI workflow that builds and packages on every tagged release keeps the published .vsix reproducible and removes manual packaging errors from the release process.

name: Package Extension
on:
 push:
 tags: ['v*']

jobs:
 package:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with:
 node-version: '20'
 - run: npm ci
 - run: npm run compile
 - run: npx @vscode/vsce package
 - uses: actions/upload-artifact@v4
 with:
 name: extension-vsix
 path: '*.vsix'

Step 13: Publish to the Marketplace and Open VSX

Publish to the Visual Studio Marketplace for VS Code users and to Open VSX for Cursor, VSCodium, and other forks that do not point at Microsoft’s proprietary marketplace by default. Both require a publisher account and a personal access token generated from their respective portals.

vsce login your-publisher-name
vsce publish

npm install -g ovsx
ovsx publish agent-pr-control-center-0.1.0.vsix -p <your-open-vsx-token>

Publishing to both registries in the same release means Cursor users get the extension without manually sideloading a .vsix file, since Cursor’s built-in extension search queries Open VSX rather than the Microsoft Marketplace.

Step 14: Write unit tests for the tree data providers

Extension code that talks to a real GitHub API is hard to test reliably in CI without hitting live rate limits or requiring a network connection during every build. Separate your data-fetching logic from your rendering logic so each can be tested independently: the fetch functions get mocked, and the tree data provider gets tested against plain in-memory arrays.

import * as assert from 'assert';
import { AgentSessionProvider } from '../providers/agentSessionProvider';

suite('AgentSessionProvider', () => {
 test('emits a change event when refreshed', (done) => {
 const provider = new AgentSessionProvider();
 provider.onDidChangeTreeData(() => done());

 provider.refresh([
 { id: '1', label: 'fix-rate-limiter', branch: 'fix/rate-limit', status: 'running', startedAt: Date.now() }
 ]);
 });

 test('getChildren returns the sessions passed to refresh', () => {
 const provider = new AgentSessionProvider();
 const sessions = [
 { id: '2', label: 'add-retry-backoff', branch: 'fix/webhook-retry', status: 'awaiting-review', startedAt: Date.now() }
 ];

 provider.refresh(sessions);
 assert.strictEqual(provider.getChildren().length, 1);
 assert.strictEqual(provider.getChildren()[0].id, '2');
 });
});

Run these with the standard @vscode/test-cli and @vscode/test-electron packages, which spin up a real (headless) instance of the editor to execute your suite against the actual extension host rather than a simulated environment. That distinction matters for extensions specifically, because a surprising number of bugs in tree views and webviews only appear inside the real extension host process, not in a plain Node.js test runner. Add a test script to package.json that points at your compiled test output, and wire it into the same GitHub Actions workflow from Step 12 so a broken provider fails the build before it ever reaches a packaged .vsix file.

For the GitHub API client itself, mock the global fetch function rather than trying to stand up a local HTTP server for every test run. A simple approach is to inject the fetch implementation as a constructor parameter on your client class, defaulting to the global fetch in production and swapping in a stub that returns fixture JSON during tests. This keeps your test suite fast (no network calls, no flakiness from GitHub’s own rate limiting) while still exercising the exact parsing and mapping logic that runs against real API responses.

Common pitfalls and how to avoid them

PitfallWhy it happensFix
Extension never activatesActivation event set too narrowly, or an unhandled error thrown synchronously in activate()Check the Debug Console for stack traces; wrap risky startup code in try/catch
Tree view stays empty after data loadsForgetting to fire _onDidChangeTreeData after updating the backing arrayAlways call refresh() immediately after mutating your data source
GitHub API returns 401Fine-grained PAT missing the pull-requests read permission, or token expiredRegenerate the token with explicit repo and PR scopes; store via SecretStorage, not settings.json
Webview renders blankMissing or misconfigured Content Security Policy blocking your script tagAdd a nonce-based CSP meta tag matching the nonce on your script element
Rate limit errors under heavy pollingPoll interval set too low across multiple monitored repositoriesDefault to 60+ seconds; use conditional requests with ETags to avoid burning quota on unchanged data
Extension works in dev host but not when packagedRelative paths to media assets break once bundled by webpack/esbuildReference assets via vscode.Uri.joinPath(context.extensionUri, ...), never a bare relative string
Marketplace rejects submission for XSS riskUntrusted PR titles or commit messages interpolated directly into webview HTMLEscape all dynamic text before injecting it into HTML strings
Secrets leak into workspace settingsStoring a PAT in settings.json because it is the fastest path during prototypingMigrate to context.secrets before your first commit, not after

Troubleshooting checklist

  • “Command not found” in the Command Palette: confirm the command ID in package.json matches exactly what you pass to vscode.commands.registerCommand, including case.
  • Extension icon missing from the Activity Bar: verify the SVG referenced in viewsContainers exists at that exact relative path and is valid SVG (VS Code silently drops malformed icons).
  • TypeScript compile errors after upgrading @types/vscode: bump your engines.vscode field to match, since type definitions are versioned against a minimum editor version.
  • Polling stops after a while with no errors logged: check whether the host machine suspended (laptop sleep); setInterval timers do not survive OS-level suspend/resume cleanly on every platform, so consider re-arming the poll in a vscode.window.onDidChangeWindowState handler.
  • GitHub API rate limit hit unexpectedly fast: confirm you are sending the Authorization header on every request; unauthenticated requests share a much lower 60 requests-per-hour limit per IP.
  • Webview content security policy blocks your own inline styles: move styles into a separate CSS file loaded via a <link> tag with the webview’s CSP-approved URI rather than inline style attributes.
  • vsce publish fails with “Missing publisher”: run vsce login again, and confirm the publisher field in package.json matches your registered Marketplace publisher ID exactly.
  • Extension works for you but not for teammates: check whether your GitHub token or repository setting was accidentally committed with a hardcoded personal value instead of reading from user-level configuration.

Advanced tips for production use

Once the core extension works, a few refinements make it durable enough for daily team use rather than a personal prototype. First, add conditional GitHub API requests using the If-None-Match header with a stored ETag from the previous response; a 304 Not Modified response does not count against your rate limit the same way a full 200 response does, which matters once you are polling several repositories at once. Second, debounce manual refresh clicks, since a user mashing the refresh command five times in two seconds should trigger one network call, not five. Third, consider adding a status bar item showing the count of unread security findings, since a sidebar view that is not currently visible is easy to forget about, while a status bar badge stays in peripheral vision at all times.

Finally, if your organization runs self-hosted agent infrastructure, consider exposing a small internal webhook receiver that your extension polls instead of hitting GitHub directly. That decouples your extension’s data source from GitHub’s rate limits entirely and lets you aggregate signals from multiple version control providers behind one consistent API, which matters if any part of your organization still runs GitLab or Bitbucket alongside GitHub.

Exposing your data through the Language Model Tools API

One feature worth adding once the core sidebar is stable is registering your PR and security data as a Language Model Tool, which lets Copilot Chat, Cursor’s chat panel, or any other chat participant call your extension directly and reason over the results in natural language, instead of requiring the developer to click into the sidebar first. Registering a tool takes only a few lines in your manifest and a callback function that returns structured content.

vscode.lm.registerTool('listOpenSecurityFindings', {
 invoke: async () => {
 const findings = await getCachedSecurityFindings();
 const summary = findings
 .map(f => `${f.severity.toUpperCase()}: ${f.title} (${f.file}:${f.line})`)
 .join('\n');
 return new vscode.LanguageModelToolResult([
 new vscode.LanguageModelTextPart(summary || 'No open security findings.')
 ]);
 }
});

Once registered, a developer can ask their chat assistant something like “are there any high severity findings on my open PRs” and get an answer sourced from your extension’s live cache, rather than the assistant guessing or hallucinating an answer with no real data behind it. Given that 46% of developers in the Stack Overflow survey cited earlier already distrust AI tool output, grounding a chat response in a tool call backed by real GitHub data is a meaningful trust signal, not just a convenience feature.

Complete working project structure

By the end of this tutorial your project directory should look like this, ready to compile, package, and publish:

agent-pr-control-center/
├── .github/
│ └── workflows/
│ └── package.yml
├── media/
│ └── robot.svg
├── src/
│ ├── extension.ts
│ ├── providers/
│ │ ├── agentSessionProvider.ts
│ │ ├── pullRequestProvider.ts
│ │ └── securityFindingsWebview.ts
│ ├── github/
│ │ └── client.ts
│ └── secrets.ts
├── .vscodeignore
├── package.json
├── tsconfig.json
└── README.md

This structure separates data providers, GitHub API access, and secret handling into distinct modules, which keeps the extension testable and makes it straightforward to add a fourth data source (a GitLab client, for instance) later without touching the tree view rendering logic at all.

How this fits the broader shift toward agentic development

VS Code’s 1.137 release added an Agent Merge Preview feature that can resolve review feedback, failed checks, and merge conflicts automatically until a pull request is mergeable, and the same release let developers work with GitHub issues and pull requests directly from the Agents window without leaving the editor. Cursor’s September 10 launch of Projects went further, introducing a coordinator agent that plans work and delegates to parallel subagents running on cloud machines, syncing shared context files across cloud and local environments. Cursor also enabled self-hosted machines on September 2, keeping codebases, build outputs, secrets, and tool execution inside an organization’s own infrastructure rather than a vendor’s cloud.

Each of these features assumes a developer who is coordinating multiple agents rather than writing every line by hand, and each one generates more state (sessions, PR comments, check runs, security findings) than the default editor UI was designed to summarize at a glance. Building a small, purpose-specific extension like the one in this tutorial is a direct, practical response to that shift, and the same tree-view-plus-webview pattern extends easily to whatever the next agent feature ships with, whether that is a deployment tracker, a cost dashboard for token usage, or a compliance audit trail for regulated teams.

Frequently asked questions

Does this extension work in Cursor without modification?

Yes. Cursor is built on the VS Code source and implements the same extension API surface, including TreeDataProvider, webviews, and SecretStorage. The one caveat is Marketplace distribution: Cursor’s built-in extension browser queries Open VSX by default, so publish there as covered in Step 13 rather than relying only on the Microsoft Marketplace.

Do I need a Cursor Teams or Enterprise plan to follow this tutorial?

No. Steps 1 through 8 and 10 through 13 work with any GitHub repository and a free-tier VS Code or Cursor installation. Step 9, which reads Rollouts and Security Review bot comments, is optional and only produces data if your repository actually has those Cursor bots enabled, which currently requires a Teams or Enterprise plan according to Cursor’s own changelog.

Why use SecretStorage instead of just reading an environment variable?

Environment variables are readable by any process running under the same user account and often get accidentally logged or echoed in terminal history. SecretStorage writes to the operating system’s native credential vault, which requires OS-level authentication to access outside your extension’s own runtime.

What happens if GitHub’s rate limit is exceeded while the extension is polling?

The fetch call in Step 4 throws an error containing a 403 or 429 status code. Wrap your poll function’s error handling to detect this specific case and back off the polling interval temporarily, since retrying immediately against an active rate limit only extends the lockout window.

Can this extension monitor multiple repositories at once?

Yes, with a small modification: change the agentPrControlCenter.repository setting from a single string to an array, and loop through each repository in your poll function, tagging each result with its source repo so the tree view can group findings accordingly.

Is it safe to publish an extension that reads pull request data from private repositories?

The extension code itself never needs to leave the user’s machine; it makes API calls directly from the user’s editor to GitHub using their own token, and no data passes through a third-party server unless you deliberately add one. Be explicit about this in your Marketplace listing description, since reviewers and security-conscious teams specifically check whether an extension proxies data through external infrastructure.

How is this different from just using the built-in GitHub Pull Requests extension?

The official GitHub Pull Requests and Issues extension handles code review, diffing, and commenting well, but it does not correlate PR activity with agent session state or security-bot findings in a single unified view. This tutorial’s extension is meant to complement that extension, not replace it, by adding the agent-and-security layer on top.

What is the realistic time investment for a developer following all 13 steps?

Budget 90 minutes to two hours for a developer already comfortable with TypeScript and the GitHub API, including time spent testing the packaged .vsix outside the debug host. Publishing to both the Marketplace and Open VSX in Step 13 adds another 15 to 20 minutes once accounts are set up, though first-time publisher verification on the Marketplace can take up to a day to process.

Related Coverage

Nadia Dubois

Nadia Dubois

AI & Innovation Editor

Nadia Dubois is the AI & Innovation Editor at Tech Insider, where she tracks the rapid evolution of artificial intelligence, from foundation models to real-world enterprise deployment. She previously covered AI and startups for La Tribune and contributed to MIT Technology Review's European coverage. Nadia specializes in generative AI, AI regulation, and the intersection of technology and European industrial policy. She holds a dual degree in Computational Linguistics and Journalism from Sciences Po Paris.

View all articles