vscode-extension-patterns
Reusable patterns for VS Code extension development.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
VS Code Extension Patterns Skill
Reusable patterns for VS Code extension development.
⚠️ Staleness Warning
VS Code APIs evolve with each monthly release. Patterns may become outdated or better alternatives may emerge.
Refresh triggers:
- VS Code major/minor releases
- New proposed APIs becoming stable
- Extension API deprecations
- Webview security policy changes
Last validated: February 2026 (VS Code 1.109+)
Check current state: VS Code API, Release Notes
Webview Dashboard
// Gather data in parallel, build HTML with async
const [health, knowledge, sync] = await Promise.all([
checkHealth(true), getKnowledgeSummary(), getSyncStatus()
]);
panel.webview.html = await getWebviewContent(health, knowledge, sync);
Key: Make getWebviewContent async if it needs directory scanning or other async ops.
TreeDataProvider for Sidebar
class WelcomeViewProvider implements vscode.WebviewViewProvider {
resolveWebviewView(webviewView: vscode.WebviewView) {
webviewView.webview.options = { enableScripts: true };
webviewView.webview.html = this.getHtmlContent();
webviewView.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'refresh': await this.refresh(); break;
}
});
}
}
// Register in extension.ts
vscode.window.registerWebviewViewProvider('alex.welcomeView', new WelcomeViewProvider());
CSP-Compliant Webview Event Handling
Problem: Inline event handlers (onclick="...") violate Content Security Policy and can be blocked.
Solution: Use data-cmd attributes with delegated event listeners:
<!-- ❌ WRONG: Inline handlers (CSP violation) -->
<button onclick="handleClick()">Click</button>
<!-- ✅ CORRECT: data-cmd pattern -->
<button data-cmd="play">Play</button>
<button data-cmd="stop">Stop</button>
// Single delegated listener for all commands
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-cmd]');
if (!target) return;
const cmd = target.getAttribute('data-cmd');
switch (cmd) {
case 'play': audio.play(); break;
case 'stop': audio.pause(); audio.currentTime = 0; break;
}
});
Benefits:
- CSP-compliant (no inline scripts)
- Single event listener (better performance)
- Easy to add new commands
- Consistent pattern across webviews
Safe Configuration Pattern
Tiered settings: Essential (🔴) → Recommended (🟡) → Auto-Approval (🟠) → Extended Thinking (🧠) → Enterprise (🔵)
Safety rules:
- Additive only — never modify/remove existing
- Check
config.inspect(key)?.globalValuebefore applying - Preview JSON before changes
- User chooses categories
- Preserve higher user values when applying recommendations
async function applySettings(settings: Record<string, unknown>) {
const config = vscode.workspace.getConfiguration();
for (const [key, value] of Object.entries(settings)) {
if (config.inspect(key)?.globalValue === undefined) {
await config.update(key, value, vscode.ConfigurationTarget.Global);
}
}
}
Comprehensive Settings Management Pattern
Pattern: Documentation-first approach for complex settings ecosystems (VS Code 1.109+ chat settings).
Steps:
-
Research Phase — Comprehensive discovery
- Grep search for all related settings in codebase
- Read MS documentation for official settings
- Identify experimental/unstable features
- Document current user configuration
-
Documentation Phase — Create reference materials before implementation
- GUIDE: Comprehensive reference (all settings, categories, use cases, warnings)
- SUMMARY: Current state snapshot (what user has, what's missing, recommendations)
- APPLIED: Change log document (what was applied, why, how to rollback)
- JSON Template: Copy-paste ready configuration file
-
Implementation Phase — Safe automated application
- Create PowerShell/shell script for automation (platform-specific)
-
Always backup before modifications (
settings.json.backup-{timestamp}) - Compare new vs existing, report changes (new, updated, skipped, preserved)
- Preserve higher values (user has 150, recommending 100? Keep 150)
- Exclude unstable features explicitly documented
-
Extension Integration — Make it permanent
- Update
setupEnvironment.tsESSENTIAL_SETTINGS, RECOMMENDED_SETTINGS, etc. - Add new categories if pattern discovered (e.g., AUTO_APPROVAL_SETTINGS)
- Integrate into Initialize/Upgrade commands (
offerEnvironmentSetup()) - Update Welcome sidebar with accurate counts
- Update
-
Validation Phase
- Test script execution (fix syntax errors iteratively)
- Verify settings applied (check VS Code settings.json)
- Document excluded settings with reasons (hooks not stable, platform-specific)
- Commit documentation files to git
Key insights from Feb 2026 implementation:
- VS Code 1.109+ has 47+ chat-related settings across 6 categories
- Hooks (
chat.hooks.enabled) marked experimental but not stable yet - Auto-approval settings reduce friction (5 settings: autoRun, fileSystem.autoApprove, terminal.*)
- Initialize/Upgrade should apply Essential + Recommended + Auto-Approval (36 total)
- PowerShell inline commands unreliable — use
.ps1file for complex scripts - Settings categories prevent overwhelming users (show 5 categories vs 47 individual settings)
When to use this pattern:
- Complex settings ecosystems (10+ related settings)
- Rapidly evolving features (experimental → stable transitions)
- User education needed (settings have non-obvious interdependencies)
- Safety-critical configuration (wrong settings break functionality)
Template structure:
docs/guides/
├── FEATURE-SETTINGS-GUIDE.md # Comprehensive reference
├── FEATURE-SETTINGS-SUMMARY.md # Current state snapshot
└── FEATURE-SETTINGS-APPLIED.md # Change log (gitignore if sensitive)
.vscode/
└── recommended-feature-settings.jsonc # Template (gitignore)
src/commands/
└── setupEnvironment.ts # Settings constants + apply logic
Benefits:
- Users understand full capability before committing
- Documentation serves as reference post-application
- Safe rollback via timestamped backups
- Extension automatically applies for new users
- Audit trail of what was applied and why
Auto-Detection with Confidence
const PATTERNS = [
{ pattern: /learned|discovered|realized/i, confidence: 0.8 },
{ pattern: /key insight|the trick is/i, confidence: 0.85 },
];
Use confidence thresholds for auto-actions. Higher threshold = fewer false positives.
Duplicate Detection
function isDuplicate(newText: string, existing: string[]): boolean {
const normalize = (s: string) => s.toLowerCase().replace(/[^\w\s]/g, '');
return existing.some(e => calculateSimilarity(normalize(newText), normalize(e)) > 0.8);
}
Portability Rules
Extensions must work on any machine:
// ✅ CORRECT: Dynamic paths
const rootPath = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
const globalPath = path.join(os.homedir(), '.alex');
// ❌ WRONG: Hardcoded paths
const rootPath = 'c:\\Development\\MyProject'; // Never!
Key utilities:
-
vscode.workspace.workspaceFolders— Current workspace -
os.homedir()— Platform-independent home -
path.join()— Cross-platform path building
Publishing Workflow
# Load PAT from .env
$env:VSCE_PAT = (Get-Content .env | Select-String "VSCE_PAT" | ForEach-Object { $_.Line.Split("=",2)[1] })
vsce publish
Version collision: Increment patch → update package.json, README badge, CHANGELOG → retry.
Goals with Streak Tracking
interface LearningGoal {
id: string;
title: string;
category: 'coding' | 'reading' | 'practice' | 'review';
targetCount: number;
currentCount: number;
type: 'daily' | 'weekly';
expiresAt: string;
}
// Auto-increment on activity
async function autoIncrementGoals(activityType: 'session' | 'insight') {
const data = await loadGoalsData();
for (const goal of data.goals) {
if (shouldIncrement(goal, activityType) && !isExpired(goal)) {
goal.currentCount = Math.min(goal.currentCount + 1, goal.targetCount);
}
}
await saveGoalsData(data);
}
SecretStorage for Sensitive Tokens
Never store secrets in settings — use VS Code's SecretStorage API:
// Module-level cache
let secretStorage: vscode.SecretStorage | null = null;
let cachedToken: string | null = null;
// Initialize during activation
export async function initSecrets(context: vscode.ExtensionContext): Promise<void> {
secretStorage = context.secrets;
cachedToken = await secretStorage.get('myExtension.apiToken') || null;
// Migration: Move token from settings to secrets
const config = vscode.workspace.getConfiguration('myExtension');
const settingsToken = config.get<string>('apiToken')?.trim();
if (settingsToken && !cachedToken) {
await secretStorage.store('myExtension.apiToken', settingsToken);
cachedToken = settingsToken;
await config.update('apiToken', undefined, vscode.ConfigurationTarget.Global);
vscode.window.showInformationMessage('Token migrated to secure storage.');
}
}
// Synchronous access to cached value
function getToken(): string | null {
return cachedToken;
}
Key points:
-
context.secrets.get()/store()/delete()are async - Cache at module level for sync access
- Migrate existing settings tokens on first run
- Mark old setting as deprecated in package.json
Webview CSP Security
Always add Content-Security-Policy when enableScripts: true:
import { getNonce } from './sanitize';
function getWebviewHtml(webview: vscode.Webview): string {
const nonce = getNonce();
return `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
style-src ${webview.cspSource} 'unsafe-inline';
script-src 'nonce-${nonce}';
img-src ${webview.cspSource} https: data:;
font-src ${webview.cspSource};
">
</head>
<body>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
// ... your code
</script>
</body>
</html>`;
}
// Nonce generator
function getNonce(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({ length: 32 }, () =>
chars.charAt(Math.floor(Math.random() * chars.length))
).join('');
}
CSP Event Delegation (onclick → data-cmd)
Problem: After adding CSP with script-src 'nonce-${nonce}', all inline event handlers (onclick, onchange) stop working because CSP blocks inline JavaScript.
Solution: Replace inline handlers with data attributes and event delegation:
<!-- ❌ BLOCKED BY CSP -->
<button onclick="cmd('upgrade')">Upgrade</button>
<button onclick="cmd('launchSkill', {skill: 'code-review'})">Review</button>
<!-- ✅ CSP-COMPLIANT -->
<button data-cmd="upgrade">Upgrade</button>
<button data-cmd="launchSkill" data-skill="code-review">Review</button>
<script nonce="${nonce}">
document.addEventListener('click', function(e) {
const el = e.target.closest('[data-cmd]');
if (el) {
e.preventDefault();
const command = el.getAttribute('data-cmd');
const skill = el.getAttribute('data-skill');
vscode.postMessage(skill ? { command, skill } : { command });
}
});
</script>
Benefits:
- Security — CSP blocks all inline scripts
- Performance — Single event listener vs many handlers
- Maintainability — Commands defined as data, not code
Webview Sandbox: postMessage Required
Problem: window.open(), location.reload(), and direct external links silently fail in sandboxed webviews.
Solution: WebView sends message to extension host; extension performs privileged action:
// In webview HTML
vscode.postMessage({ type: 'openExternal', url: 'https://example.com' });
// In extension host
panel.webview.onDidReceiveMessage(async (message) => {
if (message.type === 'openExternal') {
await vscode.env.openExternal(vscode.Uri.parse(message.url));
}
});
Key insight: WebView ↔ Extension Host communication mirrors browser Content Script ↔ Background Script patterns.
Telemetry Opt-Out Compliance
Always respect VS Code's telemetry settings:
function isTelemetryEnabled(): boolean {
// Check VS Code global setting first
if (!vscode.env.isTelemetryEnabled) {
return false;
}
// Then check extension-specific setting
const config = vscode.workspace.getConfiguration('myExtension');
return config.get<boolean>('telemetry.enabled', true);
}
function log(event: string, data?: Record<string, unknown>): void {
if (!isTelemetryEnabled()) {
return;
}
// Send telemetry...
}
Configuration Change Listeners
React to settings changes at runtime:
export function activate(context: vscode.ExtensionContext) {
// Listen for configuration changes
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('myExtension.featureA')) {
// Refresh feature A
refreshFeatureA();
}
if (e.affectsConfiguration('myExtension.telemetry')) {
// Update telemetry state
}
})
);
}
Key points:
- Use
affectsConfiguration()to filter relevant changes - Push listener to
context.subscriptionsfor cleanup - Re-read config values, don't cache indefinitely
VS Code 1.109+ Agent Platform Capabilities
VS Code 1.109 introduces a native agent platform that extensions can leverage:
Agent Files (AGENTS.md)
Extensions can ship agent definitions that VS Code auto-discovers:
<!-- .github/agents/my-agent.agent.md -->
---
name: "MyAgent"
description: "Specialized agent for domain X"
---
# MyAgent Instructions
Agent-specific instructions and knowledge go here.
Setting: chat.useAgentsMdFile: true enables automatic loading.
Skills Loading
Extensions can define skills in .github/skills/ that are auto-loaded into chat:
Setting: chat.agentSkillsLocations: [".github/skills"]
Each skill folder contains a SKILL.md (knowledge) and optional synapses.json (connections).
Chat Participant API
Register custom chat participants that users can @mention:
const participant = vscode.chat.createChatParticipant('myext.agent', async (request, context, stream, token) => {
// Access to request.prompt, request.command
// Stream responses with stream.markdown(), stream.button(), stream.reference()
stream.markdown('Hello from my agent!');
});
participant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'icon.png');
context.subscriptions.push(participant);
Tool Registration
Register tools that any chat participant can invoke:
const tool = vscode.lm.registerTool('myext-searchDocs', {
async invoke(options, token) {
const query = options.input.query;
// Perform tool action
return new vscode.LanguageModelToolResult([
new vscode.LanguageModelTextPart(JSON.stringify(results))
]);
}
});
context.subscriptions.push(tool);
Declare in package.json:
{
"contributes": {
"languageModelTools": [{
"name": "myext-searchDocs",
"displayName": "Search Documentation",
"modelDescription": "Searches project documentation for relevant content",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query" }
},
"required": ["query"]
}
}]
}
}
Extended Thinking
Models supporting extended thinking can be configured per-model:
{
"claude-opus-4-*.extendedThinkingEnabled": true,
"claude-opus-4-*.thinkingBudget": 16384
}
MCP Integration
VS Code 1.109+ supports Model Context Protocol servers:
Setting: chat.mcp.gallery.enabled: true
MCP servers extend AI capabilities with external tools (Azure, GitHub, databases).
Key 1.109 Settings Summary
| Setting | Value | Purpose |
|---|---|---|
chat.agent.enabled |
true |
Enable custom agents |
chat.agentSkillsLocations |
[".github/skills"] |
Auto-load skills |
chat.useAgentsMdFile |
true |
Use AGENTS.md |
chat.mcp.gallery.enabled |
true |
MCP tool access |
Integration Audit Checklist
10-category audit scoring system (5 points each, 50 total):
| # | Category | What to Check |
|---|---|---|
| 1 | Activation Events | package.json activationEvents match actual needs |
| 2 | Extension Context | context.subscriptions, secrets, globalState usage |
| 3 | Disposable Management | All disposables pushed to subscriptions |
| 4 | Command Registration | Commands in package.json match registerCommand |
| 5 | Configuration Access | getConfiguration usage, onDidChangeConfiguration |
| 6 | Webview Security | CSP policies, nonce usage, enableScripts |
| 7 | Language Model/Chat | vscode.lm patterns, tool registration |
| 8 | Telemetry | vscode.env.isTelemetryEnabled respected |
| 9 | Error Handling | try/catch patterns, error type handling |
| 10 | File System | vscode.workspace.fs vs Node.js fs |
Quick wins (high impact, low effort):
- Telemetry opt-out: Check
vscode.env.isTelemetryEnabled - CSP on webviews: Add Content-Security-Policy with nonce
- Config listeners: Add
onDidChangeConfigurationfor runtime updates - Secret storage: Use
context.secretsinstead of settings for tokens
Scoring:
- 45-50: Excellent — Ready for publish
- 40-44: Good — Minor fixes
- 35-39: Fair — Address before publish
- <35: Needs Work — Major refactoring
When to apply: Before marketplace publishing, after major features, quarterly reviews.
Synapses
See synapses.json for connections.
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-03-11
- Index updated
- 2026-03-11
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!