mit einem Klick
vscode-extension-patterns
Reusable patterns for VS Code extension development.
Menü
Reusable patterns for VS Code extension development.
| name | vscode-extension-patterns |
| description | Reusable patterns for VS Code extension development. |
Reusable patterns for VS Code extension development.
VS Code APIs evolve with each monthly release. Patterns may become outdated or better alternatives may emerge.
Refresh triggers:
Last validated: February 2026 (VS Code 1.109+)
Check current state: VS Code API, Release Notes
// 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.
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());
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:
Tiered settings: Essential (🔴) → Recommended (🟡) → Auto-Approval (🟠) → Extended Thinking (🧠) → Enterprise (🔵)
Safety rules:
config.inspect(key)?.globalValue before applyingasync 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);
}
}
}
Pattern: Documentation-first approach for complex settings ecosystems (VS Code 1.109+ chat settings).
Steps:
Research Phase — Comprehensive discovery
Documentation Phase — Create reference materials before implementation
Implementation Phase — Safe automated application
settings.json.backup-{timestamp})Extension Integration — Make it permanent
setupEnvironment.ts ESSENTIAL_SETTINGS, RECOMMENDED_SETTINGS, etc.offerEnvironmentSetup())Validation Phase
Key insights from Feb 2026 implementation:
chat.hooks.enabled) marked experimental but not stable yet.ps1 file for complex scriptsWhen to use this pattern:
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:
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.
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);
}
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 workspaceos.homedir() — Platform-independent homepath.join() — Cross-platform path building# 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.
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);
}
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 asyncAlways 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('');
}
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:
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.
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...
}
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:
affectsConfiguration() to filter relevant changescontext.subscriptions for cleanupVS Code 1.109 introduces a native agent platform that extensions can leverage:
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.
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).
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);
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"]
}
}]
}
}
Models supporting extended thinking can be configured per-model:
{
"claude-opus-4-*.extendedThinkingEnabled": true,
"claude-opus-4-*.thinkingBudget": 16384
}
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).
| 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 |
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):
vscode.env.isTelemetryEnabledonDidChangeConfiguration for runtime updatescontext.secrets instead of settings for tokensScoring:
When to apply: Before marketplace publishing, after major features, quarterly reviews.
See synapses.json for connections.
Defense-in-depth, PII protection, secrets scanning, and secure packaging for distributed software
Systematic testing for confidence without over-testing — the right test at the right level
Generate consistent visual character references across multiple scenarios using Flux and nano-banana-pro on Replicate
Create professional ultra-wide cinematic banners for GitHub READMEs using Flux and Ideogram models with typography options
Generate professional presentations using the Gamma API with expert storytelling consulting based on Duarte methodology.
Intelligent project persona identification using priority chain detection with LLM and heuristic fallback