Build a VS Code AI Extension: 13 Steps, Works in Cursor [2026]

Building a Visual Studio Code extension used to mean writing boilerplate for a feature you could ship in an afternoon. That changed when Microsoft opened up the Language Model API inside the VS Code Extension API, letting any extension call an AI model directly instead of shipping its own inference layer. The same extension format runs unmodified inside Cursor, since Cursor is a fork of VS Code and reuses its Extensions panel and marketplace format. That means one codebase, one manifest, and one publish step gets you a working AI feature in both editors.

This tutorial builds a real, working extension called Explain Selected Code from scratch. It reads whatever code a developer highlights, sends it to a language model through the official API, and prints a plain-English explanation right next to the code. By the end you will have a packaged .vsix file installed locally in both VS Code 1.140 (the build GitHub Copilot in VS Code reached as of September 2026) and Cursor, plus the knowledge to swap in test generation, refactor suggestions, or any other AI-assisted command you want to build next.

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 a VS Code extension with the Language Model API right now

Search interest in “vscode extension” development sits around 1,600 monthly searches in the US, and it has stayed steady rather than declining, because the extension model keeps evolving. The Language Model API is the newest reason to look again. Instead of bundling an OpenAI or Anthropic SDK, managing API keys, and eating the network cost yourself, an extension calls vscode.lm.selectChatModels and borrows whatever model the user already has configured in their editor’s chat picker. The user’s existing subscription and quota cover the request. Your extension code stays small, and you never touch a raw API key.

The timing also lines up with a wave of model upgrades. GitHub Copilot in VS Code reached version 1.140 in September 2026, and that release brought Claude Sonnet 5.5 into the Copilot VS Code extension alongside GPT-6.1 Sol, now available to Copilot Pro+, Max, Business, and Enterprise users, so selectChatModels can resolve requests against a noticeably newer lineup than the gpt-4o, gpt-4o-mini, o1, o1-mini, and claude-3.5-sonnet families that VS Code’s own AI extensibility guide originally documented. Cursor, which runs the identical extension host, exposes a separate and considerably newer model picker to end users, currently offering Grok 4.7 (released September 21, 2026), the GPT-5.6 family (Sol, Terra, Luna), Claude Fable 5.1, Claude Opus 5.5, Gemini 3.1 Pro, and Cursor’s own Composer 2.5 agent model. Your extension code does not need to know which specific model is active. It asks for a family, and the host editor resolves that request against whatever is installed and licensed.

That gap between what VS Code’s public documentation lists and what Cursor actually ships to users is the core tension this tutorial deals with directly. An extension written against the wrong assumption, that only Copilot-branded models exist, breaks the moment someone installs it in Cursor. Building the fallback logic correctly from the start avoids that entirely, and it is the difference between an extension that works in one editor and one that works everywhere built on the same open extension host.

There is also a distribution argument for choosing this project over a plain code snippet extension. Both the Visual Studio Marketplace and Open VSX index tens of thousands of extensions, and a listing with zero differentiated functionality struggles to get discovered. An extension that solves a genuine friction point, understanding unfamiliar code quickly during a review or an onboarding task, without requiring the installer to configure a separate API key, clears a much lower adoption bar than a tool that demands upfront setup. That is part of why Language Model API-based extensions have grown steadily on both registries through 2026: the entire barrier to trying one is a single click on Install.

How a request actually travels from your extension to the model

It helps to understand what happens between the moment your extension calls sendRequest and the moment text starts streaming back, because half of the troubleshooting steps later in this tutorial only make sense once you can picture the path. Your extension code runs inside the editor’s extension host process, a sandboxed Node.js environment separate from the main UI process. When you call vscode.lm.selectChatModels, that request does not go directly to OpenAI, Anthropic, or xAI. It goes to the editor’s own model-routing layer, the same layer that powers the built-in chat panel.

That routing layer checks two things before it lets your request through: whether the user has granted your specific extension permission to use language models at all, and which underlying provider is currently configured to answer that vendor and family combination. Only after both checks pass does the editor forward your prompt to the actual model provider’s API, wait for tokens, and stream them back down into your extension’s sendRequest call. This is why an extension never needs its own API key: the editor is acting as a trusted broker on your behalf, the same way a browser broker mediates access to a webcam or microphone rather than letting a website talk to camera hardware directly.

This brokered design has a practical consequence worth internalizing: your extension has no visibility into which literal API endpoint served the request, no access to raw billing data, and no ability to bypass the user’s configured model. If you need an extension that talks to a specific provider regardless of what the user has configured in their editor, the Language Model API is the wrong tool, and you would fall back to bundling a provider SDK with your own API key management instead, accepting the added maintenance burden that comes with it.

Automating packaging and publishing with GitHub Actions

Running vsce package and vsce publish by hand works for a first release, but any extension you intend to update regularly benefits from a CI pipeline that packages and publishes automatically whenever you push a version tag. This keeps the manual publish token off your local machine most of the time and removes the risk of forgetting a step, like bumping the version number, before a release.

name: Publish Extension

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    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 vsce publish -p ${{ secrets.VSCE_PAT }}
      - run: npx ovsx publish -p ${{ secrets.OVSX_PAT }}

Store both tokens, the Visual Studio Marketplace personal access token and the Open VSX access token, as encrypted repository secrets rather than plain environment variables in the workflow file. Tag a release with git tag v0.0.2 && git push origin v0.0.2 and the workflow packages, compiles, and publishes to both registries without any manual step. For a team publishing several AI-powered extensions from the same organization, this pattern also makes it straightforward to add a test job ahead of the publish job, so a broken build never reaches the Marketplace in the first place.

Prerequisites and required versions

Confirm each of these before starting. Version mismatches are the single biggest source of “my extension won’t activate” support threads, so treat this list as a checklist rather than a suggestion.

RequirementMinimum versionWhy it matters
Node.js20.x LTS or newerYeoman generator and esbuild both require modern Node
npm10.xBundled with Node 20; needed for dependency install
VS Code1.137Current release documented against the Language Model API
Cursorlatest stable build (2026.09 channel)Shares the VS Code 1.137 extension host
@vscode/vsce3.x (installed via npm)Packages and publishes the .vsix file
yo + generator-codelatest (installed via npm)Scaffolds the extension skeleton
TypeScript5.4 or newerExtension API type definitions require current TS
Git2.40+Version control for the extension source, optional but recommended

You do not need an OpenAI, Anthropic, or xAI API key for this tutorial. That is the entire point of building against the Language Model API instead of a raw SDK: the host editor supplies the model access, and your extension code stays provider-agnostic. Confirm your editor version before you start by opening the About dialog: in VS Code, go to Help > About, and in Cursor check Cursor > About Cursor. Both should report a build derived from the 1.140 extension host line, the version GitHub Copilot in VS Code reached as of September 2026.

Step 1: Scaffold the extension with Yeoman

Microsoft’s own extension generator is still the fastest path to a working skeleton. Install the two global packages you need, then run the generator.

npm install -g yo generator-code @vscode/vsce
yo code

The generator asks a series of questions. Answer them as follows for this project:

  • What type of extension: New Extension (TypeScript)
  • Name: explain-selected-code
  • Identifier: leave the default
  • Description: Explains highlighted code using the VS Code Language Model API
  • Initialize a git repository: Yes
  • Bundle with esbuild: Yes
  • Package manager: npm

Once the generator finishes, per the official Your First Extension walkthrough, open the folder in VS Code (code explain-selected-code) and run npm install to pull in the generated dependencies. You now have a working “Hello World” extension you can press F5 to launch in a new Extension Development Host window. Confirm that base case works before touching any AI code. It is much easier to debug a broken model call in isolation than to debug it alongside a broken scaffold.

Step 2: Understand the generated file structure

Before writing any code, look at what the generator produced. Three files matter most for this project:

FilePurpose
package.jsonExtension manifest: name, activation events, contributed commands
src/extension.tsEntry point; exports activate() and deactivate()
.vscode/launch.jsonDebug configuration used when you press F5

The manifest is the file the extension host reads first. Every command, keybinding, and permission your extension needs gets declared there before a single line of TypeScript runs. If you have built a browser extension before, this maps almost directly onto a manifest.json file: declarative capabilities first, imperative logic second.

Step 3: Declare the command in package.json

Open package.json and add a contributes.commands entry. Replace the generated contributes block with this:

"activationEvents": [],
"main": "./dist/extension.js",
"contributes": {
  "commands": [
    {
      "command": "explain-selected-code.explain",
      "title": "Explain Selected Code"
    }
  ]
},
"engines": {
  "vscode": "^1.90.0"
}

Consult the extension manifest reference for the full list of contribution points available. You do not need to declare a special permission field for vscode.lm in the manifest itself. Access control happens at runtime: the first time your extension calls selectChatModels, the host editor prompts the user to approve model access for that specific extension, the same way it would prompt for filesystem or network access. That consent prompt is a deliberate design choice so a malicious extension cannot silently burn a user’s model quota without the user knowing which extension is responsible.

Step 4: Read the active editor’s selection

Open src/extension.ts and replace the generated activate function body. The first working piece is grabbing whatever text the user has highlighted:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  const disposable = vscode.commands.registerCommand(
    'explain-selected-code.explain',
    async () => {
      const editor = vscode.window.activeTextEditor;
      if (!editor) {
        vscode.window.showInformationMessage('Open a file and select some code first.');
        return;
      }

      const selection = editor.selection;
      const selectedText = editor.document.getText(selection);

      if (!selectedText.trim()) {
        vscode.window.showInformationMessage('Select the code you want explained.');
        return;
      }

      // Model call goes here (Step 5)
    }
  );

  context.subscriptions.push(disposable);
}

export function deactivate() {}

Compile with npm run compile to catch any TypeScript errors before moving on. This step alone is worth testing in isolation: press F5, open a file with some code in it, highlight a block, and run the command from the Command Palette (Cmd/Ctrl+Shift+P) to confirm the selection text shows up correctly. Add a temporary console.log(selectedText) if you want to verify it in the Debug Console before adding the model call.

Step 5: Call the Language Model API

This is the core of the tutorial. Replace the // Model call goes here comment with the actual API call. The pattern is: select a model family, build a prompt, send it, and stream the response.

const [model] = await vscode.lm.selectChatModels({
  vendor: 'copilot',
  family: 'gpt-4o'
});

if (!model) {
  vscode.window.showErrorMessage(
    'No language model available. Sign in to GitHub Copilot Chat or select a model in your editor\'s chat panel.'
  );
  return;
}

const craftedPrompt = [
  vscode.LanguageModelChatMessage.User(
    `Explain what this code does in plain English, in 3-5 sentences, ` +
    `for a developer who has never seen it before:\n\n${selectedText}`
  )
];

const chatResponse = await model.sendRequest(
  craftedPrompt,
  {},
  new vscode.CancellationTokenSource().token
);

let explanation = '';
for await (const fragment of chatResponse.text) {
  explanation += fragment;
}

const panel = vscode.window.createOutputChannel('Code Explanation');
panel.appendLine(explanation);
panel.show();

The vendor: 'copilot' parameter targets whatever provider the host editor has configured for Copilot-compatible model access; in VS Code that resolves against GitHub Copilot’s model list, and in Cursor’s own extension host it resolves against Cursor’s active model selection instead. If you want the extension to work identically regardless of which editor loaded it, omit the vendor filter and let selectChatModels({}) return whatever the first available match is, then check model.family in your logs to see what actually got selected.

Step 6: Handle streaming responses properly

The example above buffers the whole response before displaying it, which works but wastes the API’s built-in streaming support. For a better user experience, update the output channel as each fragment arrives instead of waiting for the full response:

const panel = vscode.window.createOutputChannel('Code Explanation');
panel.clear();
panel.show();

for await (const fragment of chatResponse.text) {
  panel.append(fragment);
}

This small change is what makes the difference between an extension that feels frozen for three or four seconds and one that feels alive from the first token. Streaming also matters for longer explanations, since a 500-token response can take noticeably longer to fully generate than a short one, and users should see progress immediately rather than staring at a blank panel. If you plan to render the explanation somewhere richer than a plain output channel, such as a webview panel, apply the same principle: append fragments to the webview’s DOM as they arrive rather than waiting for the full string.

Step 7: Add error handling for model access denial and rate limits

Two failure modes are common enough that skipping error handling here will generate support tickets on day one: the user declining the model access consent prompt, and the model provider returning a quota or content-filtering error. Wrap the sendRequest call in a try/catch that distinguishes between them:

try {
  const chatResponse = await model.sendRequest(
    craftedPrompt,
    {},
    new vscode.CancellationTokenSource().token
  );
  for await (const fragment of chatResponse.text) {
    panel.append(fragment);
  }
} catch (err) {
  if (err instanceof vscode.LanguageModelError) {
    if (err.code === 'NoPermissions') {
      vscode.window.showWarningMessage(
        'Model access was declined. Run the command again and approve the permission prompt.'
      );
    } else if (err.code === 'Blocked') {
      vscode.window.showWarningMessage(
        'The request was blocked by content filtering. Try selecting a smaller code block.'
      );
    } else {
      vscode.window.showErrorMessage(`Model request failed: ${err.message}`);
    }
  } else {
    vscode.window.showErrorMessage(`Unexpected error: ${(err as Error).message}`);
  }
}

vscode.LanguageModelError is a dedicated error class the API returns specifically so extensions do not have to guess whether a failure was a network problem, a permissions problem, or a content policy problem. Checking err.code lets you show the user an actionable message instead of a generic “something went wrong” toast that leaves them unsure what to do next.

Step 8: Test inside the Extension Development Host

Press F5 from the main VS Code window. This launches a second Extension Development Host window with your unpublished extension already loaded. In that window:

  1. Open any file (a JavaScript, Python, or TypeScript file works well for testing)
  2. Highlight a function or code block
  3. Open the Command Palette and run Explain Selected Code
  4. Approve the model access consent prompt when it appears (first run only)
  5. Watch the Output panel populate with a streamed explanation

Set breakpoints directly in extension.ts in the main window; the debugger attaches automatically to the Extension Development Host process, so you can step through the selection logic and inspect the prompt text before it gets sent. If nothing happens when you run the command, check the Debug Console in the main window first, since most activation errors surface there rather than as a visible notification.

Step 9: Sample output to expect

Here is a realistic example of what the extension produces against a small quicksort function:

Input (selected code):
function quickSort(arr) {
  if (arr.length <= 1) return arr;
  const pivot = arr[0];
  const left = arr.slice(1).filter(x => x < pivot);
  const right = arr.slice(1).filter(x => x >= pivot);
  return [...quickSort(left), pivot, ...quickSort(right)];
}

Output (Code Explanation panel):
This function implements the quicksort algorithm recursively. It picks
the first element of the array as the pivot, then splits the remaining
elements into two groups: those smaller than the pivot and those equal
to or greater than it. It recursively sorts both groups and combines
them with the pivot placed in the middle. The base case stops recursion
once an array has zero or one elements, since those are already sorted.

The exact wording will vary between model families. GPT-4o tends to produce tighter, more clipped explanations, while Claude-family models tend to add a bit more structure with numbered steps. Since your prompt does not hardcode a model, the phrasing shifts depending on what the user has selected as their active chat model, which is expected behavior rather than a bug to fix.

Step 10: Package the extension into a .vsix file

Once the extension works in the debug host, package it for local installation or marketplace publishing using vsce, the official CLI:

npx vsce package

This produces explain-selected-code-0.0.1.vsix in your project root. If vsce complains about a missing repository field or license, add both to package.json, a "repository" URL and a "license" field, since the packager treats those as warnings rather than blockers, but the Marketplace will reject a submission missing them later. Better to fix it now than after your first publish attempt fails.

Step 11: Install the .vsix in both VS Code and Cursor

Because Cursor shares the VS Code extension host, the exact same file installs in both editors without any changes. Install it from the command line:

# VS Code
code --install-extension explain-selected-code-0.0.1.vsix

# Cursor
cursor --install-extension explain-selected-code-0.0.1.vsix

Or install manually through the UI: open the Extensions panel with Cmd/Ctrl+Shift+X, click the … menu at the top of the panel, choose Install from VSIX, and select the file. According to Cursor’s own documentation, once installed this way the extension activates immediately, with no reload required for most extensions, though ones that hook deeply into the editor lifecycle sometimes need a manual reload the first time.

Step 12: Verify model access resolves correctly in Cursor

This step catches the most common cross-editor bug. Run the command inside Cursor and check which model actually served the request by temporarily logging model.family and model.vendor to the Output channel. Because Cursor’s model picker (Grok 4.7, GPT-5.6 Sol/Terra/Luna, Claude Fable 5.1, Claude Opus 5.5, Gemini 3.1 Pro, Composer 2.5) is a different pool from the VS Code Copilot model list, a hardcoded vendor: 'copilot' filter can return no match at all inside Cursor if the user has no Copilot-compatible model active. The fix is to fall back gracefully:

let [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' });

if (!model) {
  // Fall back to any available model, regardless of vendor
  [model] = await vscode.lm.selectChatModels({});
}

if (!model) {
  vscode.window.showErrorMessage(
    'No chat model is available in this editor. Configure a model in the chat panel and try again.'
  );
  return;
}

This two-tier fallback is the single line of defense that keeps your extension from feeling broken the moment someone installs it in Cursor instead of VS Code proper. It costs nothing in the common case, since the first filtered request still resolves instantly when a matching model exists, and it only falls through to the broader search when the strict filter comes back empty.

Step 13: Publish to the Marketplace or Open VSX

Once you’re satisfied with local testing, publish through vsce to the official Visual Studio Marketplace, or through ovsx to Open VSX, the open registry Cursor and other VS Code-compatible editors read from when the Microsoft Marketplace terms don’t apply:

# Publish to the Visual Studio Marketplace (requires a publisher account and PAT)
npx vsce publish

# Publish to Open VSX (requires a separate access token)
npx ovsx publish explain-selected-code-0.0.1.vsix -p your-ovsx-token

Publishing to both registries maximizes reach, since some editors in the VS Code ecosystem pull exclusively from Open VSX for licensing reasons rather than the Microsoft Marketplace. Neither publish command overwrites the other, so running both in sequence is the standard practice for extension authors targeting more than one editor family.

Five common pitfalls when building AI-powered extensions

  1. Forgetting the cancellation token. sendRequest requires a CancellationToken as its third argument. Passing a bare undefined throws a type error in strict TypeScript configurations; always construct one with new vscode.CancellationTokenSource().token, and dispose of the source when the command completes if you’re calling it repeatedly.
  2. Sending the entire open file instead of the selection. A common mistake is reading editor.document.getText() with no arguments, which returns the whole file rather than just the highlighted block. Always pass the selection range explicitly, and add a guard that shows a message when the selection is empty.
  3. Hardcoding one vendor and family. As covered in Step 12, hardcoding vendor: 'copilot' works in VS Code but can silently fail in Cursor or any other fork with a different model backend. Always implement a fallback path.
  4. Blocking the UI thread waiting for the full response. Buffering the entire streamed response before showing anything, skipping Step 6, makes the extension feel unresponsive on longer completions. Stream fragments into the UI as they arrive instead.
  5. Not handling the consent-denial case. If a user declines the model access permission prompt, sendRequest throws a LanguageModelError with code NoPermissions. Extensions that don’t catch this specific case crash with an unhandled promise rejection visible in the Developer Tools console, which looks unprofessional even though the actual failure is just the user saying no.

Advanced tips: going beyond a single command

Once the basic explain command works, a few extensions of the pattern are worth trying. A chat participant registered through vscode.chat.createChatParticipant lets users invoke your extension by typing @explain directly in the built-in chat panel instead of running a Command Palette action, which is a more discoverable entry point for most users. A code action provider can surface an “Explain this” lightbulb any time the cursor sits inside a function, without requiring a manual selection at all. And registering a language model tool via vscode.lm.registerTool lets other agents, including the built-in Copilot agent mode, call your extension’s logic as a callable tool during a longer autonomous task, rather than only as a standalone user-triggered command.

For production extensions, cache the result of selectChatModels at activation time rather than calling it fresh on every command invocation. Model availability rarely changes mid-session, and the repeated lookup adds a small but unnecessary delay to every single command run. It is also worth trimming very large selections before sending them to the model; a 2,000-line selection not only risks hitting a context limit but also costs the user more of their quota than a focused, well-scoped block would.

Another pattern worth building once the basics work is progress reporting through vscode.window.withProgress, wrapping the entire model call so a small progress notification appears in the status bar area rather than leaving the user guessing whether the command is still running. Combined with a CancellationToken tied to that same progress object, a user can cancel a long-running explanation mid-stream, which matters more than it sounds once you start sending larger selections that take several seconds to fully resolve. Finally, consider persisting the last few explanations in workspace state via context.workspaceState, so a user re-running the command against the exact same selection gets an instant cached result instead of a fresh, billable model call.

Complete working project reference

The finished src/extension.ts, combining every step above into one file, looks like this:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  const disposable = vscode.commands.registerCommand(
    'explain-selected-code.explain',
    async () => {
      const editor = vscode.window.activeTextEditor;
      if (!editor) {
        vscode.window.showInformationMessage('Open a file and select some code first.');
        return;
      }

      const selectedText = editor.document.getText(editor.selection);
      if (!selectedText.trim()) {
        vscode.window.showInformationMessage('Select the code you want explained.');
        return;
      }

      let [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', family: 'gpt-4o' });
      if (!model) {
        [model] = await vscode.lm.selectChatModels({});
      }
      if (!model) {
        vscode.window.showErrorMessage('No chat model available in this editor.');
        return;
      }

      const craftedPrompt = [
        vscode.LanguageModelChatMessage.User(
          `Explain what this code does in plain English, in 3-5 sentences:\n\n${selectedText}`
        )
      ];

      const panel = vscode.window.createOutputChannel('Code Explanation');
      panel.clear();
      panel.show();

      try {
        const chatResponse = await model.sendRequest(
          craftedPrompt,
          {},
          new vscode.CancellationTokenSource().token
        );
        for await (const fragment of chatResponse.text) {
          panel.append(fragment);
        }
      } catch (err) {
        if (err instanceof vscode.LanguageModelError) {
          if (err.code === 'NoPermissions') {
            vscode.window.showWarningMessage('Model access declined. Run the command again to approve.');
          } else {
            vscode.window.showErrorMessage(`Model request failed: ${err.message}`);
          }
        } else {
          vscode.window.showErrorMessage(`Unexpected error: ${(err as Error).message}`);
        }
      }
    }
  );

  context.subscriptions.push(disposable);
}

export function deactivate() {}

Copy this into a fresh scaffold from Step 1, run npm run compile, press F5, and you have the complete tutorial project running end to end in under 15 minutes once the tooling is installed. From this base, swapping the prompt string is enough to turn the same skeleton into a test generator, a comment writer, or a refactor suggestion tool.

Troubleshooting: issues developers commonly hit

Most of the failures below surface during the first hour of development, usually right after the scaffold step or right after the first attempt to call the Language Model API. Work through this table top to bottom before assuming the API itself is broken, since the overwhelming majority of reported issues in the VS Code extension community trace back to a manifest mismatch, a stale type definition, or a missing consent grant rather than an actual defect in the platform.

SymptomLikely causeFix
Command doesn’t appear in Command PaletteCommand ID mismatch between package.json and registerCommandConfirm both strings match exactly, including hyphens
“No language model available” every timeNo Copilot Chat account signed in, or no model selected in chat panelSign in to Copilot Chat, or select any model in the chat picker first
Extension works in VS Code but not CursorHardcoded vendor filter finds no match in Cursor’s model poolAdd the two-tier fallback from Step 12
TypeScript error: Property ‘lm’ does not exist on typeOutdated @types/vscode packageRun npm install @types/vscode@latest
Output panel stays blankStreaming loop never awaited or was skipped after an early returnConfirm the for-await loop runs inside the try block, not after it
vsce package fails with missing repository fieldpackage.json lacks a repository URLAdd a “repository” field pointing to your git remote
Consent prompt never appearsExtension was already granted access in a previous test runReset via Command Palette: “Developer: Reset Extension Host”
.vsix installs but command is greyed outactivationEvents list is too restrictiveLeave activationEvents as an empty array so VS Code infers activation from contributed commands
Explanation cuts off mid-sentenceModel hit a token limit on a very large selectionReduce selection size or lower the requested explanation length in the prompt
ovsx publish rejects the packageMissing publisher namespace registration on Open VSXCreate and verify a namespace at open-vsx.org before publishing

Frequently asked questions

Does the Language Model API cost anything extra to use?

No. Your extension code does not manage billing at all. Model usage is billed against whatever subscription or quota the end user already has active in their editor, whether that’s a GitHub Copilot plan or a Cursor plan. Since April 2026, Microsoft ships GitHub Copilot Chat as a built-in VS Code extension rather than a separate install, so most VS Code users already arrive with a Copilot-compatible model configured out of the box. You never handle an API key or a billing account inside the extension.

Can this extension work without GitHub Copilot installed?

It depends on the vendor filter. If you request vendor: 'copilot' specifically and the user has no Copilot account, the request returns no model, though that scenario is rarer on VS Code proper now that Microsoft folded GitHub Copilot Chat into VS Code as a built-in extension back in April 2026 instead of a separate install. Using the broader selectChatModels({}) fallback still covers editors and configurations where a different model provider is active, which is why Step 12’s fallback pattern matters for real-world reliability.

Will this same .vsix file install in Windsurf or other VS Code forks?

In most cases yes, since any fork built on the open-source VS Code (Code – OSS) core reuses the identical extension host and manifest format. Model resolution behavior specifically depends on what that fork implements behind vscode.lm, so always test the actual target editor rather than assuming full parity.

Do I need to publish to the Marketplace to use this extension myself?

No. Step 11 covers local installation directly from the packaged .vsix file, which is enough for personal use or sharing within a small team. Marketplace or Open VSX publishing, covered in Step 13, is only necessary for public distribution.

Why does my prompt sometimes get blocked with a “Blocked” error code?

Content filtering on the model provider’s side can reject certain inputs, particularly very large code blocks or code containing strings that trip automated safety filters. Selecting a smaller, more focused code block for explanation typically resolves this.

Can I use this pattern to generate unit tests instead of explanations?

Yes, and it requires almost no structural change. Swap the prompt text in the LanguageModelChatMessage.User call to ask for tests instead of an explanation, and route the streamed output into a new file via vscode.workspace.applyEdit instead of an output channel. The model selection, error handling, and Cursor fallback logic all stay identical.

Does the extension need internet access permissions declared anywhere?

No explicit network permission needs to be declared in the manifest. The Language Model API handles all network communication internally within the host editor’s process, and the only permission the user sees is the model-access consent prompt described in Step 3.

What happens if the user switches models mid-task in Cursor?

Since selectChatModels is called fresh at the start of each command run in the reference implementation, a model switch in the editor’s chat picker is picked up automatically on the next invocation. There is no stale reference to worry about unless you deliberately cache the model object across sessions, in which case you would need to re-resolve it periodically, ideally on an interval measured in minutes rather than seconds, since re-querying on every keystroke or every few hundred milliseconds provides no real benefit and simply adds unnecessary overhead to the extension host.

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